@marktoflow/core 2.0.0-alpha.12
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 +307 -0
- package/dist/bundle.d.ts +43 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +202 -0
- package/dist/bundle.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +27 -0
- package/dist/config.js.map +1 -0
- package/dist/core-tools.d.ts +39 -0
- package/dist/core-tools.d.ts.map +1 -0
- package/dist/core-tools.js +58 -0
- package/dist/core-tools.js.map +1 -0
- package/dist/costs.d.ts +182 -0
- package/dist/costs.d.ts.map +1 -0
- package/dist/costs.js +464 -0
- package/dist/costs.js.map +1 -0
- package/dist/credentials.d.ts +162 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +646 -0
- package/dist/credentials.js.map +1 -0
- package/dist/engine.d.ts +243 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1453 -0
- package/dist/engine.js.map +1 -0
- package/dist/env.d.ts +59 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +256 -0
- package/dist/env.js.map +1 -0
- package/dist/failover.d.ts +43 -0
- package/dist/failover.d.ts.map +1 -0
- package/dist/failover.js +53 -0
- package/dist/failover.js.map +1 -0
- package/dist/filewatcher.d.ts +32 -0
- package/dist/filewatcher.d.ts.map +1 -0
- package/dist/filewatcher.js +92 -0
- package/dist/filewatcher.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/logging.d.ts +62 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +211 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp-loader.d.ts +29 -0
- package/dist/mcp-loader.d.ts.map +1 -0
- package/dist/mcp-loader.js +60 -0
- package/dist/mcp-loader.js.map +1 -0
- package/dist/metrics.d.ts +19 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +65 -0
- package/dist/metrics.js.map +1 -0
- package/dist/models.d.ts +1686 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +333 -0
- package/dist/models.js.map +1 -0
- package/dist/parser.d.ts +40 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +532 -0
- package/dist/parser.js.map +1 -0
- package/dist/permissions.d.ts +49 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +286 -0
- package/dist/permissions.js.map +1 -0
- package/dist/plugins.d.ts +105 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +182 -0
- package/dist/plugins.js.map +1 -0
- package/dist/prompt-loader.d.ts +47 -0
- package/dist/prompt-loader.d.ts.map +1 -0
- package/dist/prompt-loader.js +268 -0
- package/dist/prompt-loader.js.map +1 -0
- package/dist/queue.d.ts +114 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +385 -0
- package/dist/queue.js.map +1 -0
- package/dist/rollback.d.ts +117 -0
- package/dist/rollback.d.ts.map +1 -0
- package/dist/rollback.js +374 -0
- package/dist/rollback.js.map +1 -0
- package/dist/routing.d.ts +144 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +457 -0
- package/dist/routing.js.map +1 -0
- package/dist/scheduler.d.ts +91 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +259 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/script-tool.d.ts +22 -0
- package/dist/script-tool.d.ts.map +1 -0
- package/dist/script-tool.js +90 -0
- package/dist/script-tool.js.map +1 -0
- package/dist/sdk-registry.d.ts +94 -0
- package/dist/sdk-registry.d.ts.map +1 -0
- package/dist/sdk-registry.js +328 -0
- package/dist/sdk-registry.js.map +1 -0
- package/dist/security.d.ts +155 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +362 -0
- package/dist/security.js.map +1 -0
- package/dist/state.d.ts +67 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +276 -0
- package/dist/state.js.map +1 -0
- package/dist/templates.d.ts +70 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +244 -0
- package/dist/templates.js.map +1 -0
- package/dist/tool-base.d.ts +54 -0
- package/dist/tool-base.d.ts.map +1 -0
- package/dist/tool-base.js +43 -0
- package/dist/tool-base.js.map +1 -0
- package/dist/tool-registry.d.ts +24 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +164 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/tools/custom-tool.d.ts +16 -0
- package/dist/tools/custom-tool.d.ts.map +1 -0
- package/dist/tools/custom-tool.js +85 -0
- package/dist/tools/custom-tool.js.map +1 -0
- package/dist/tools/mcp-tool.d.ts +16 -0
- package/dist/tools/mcp-tool.d.ts.map +1 -0
- package/dist/tools/mcp-tool.js +98 -0
- package/dist/tools/mcp-tool.js.map +1 -0
- package/dist/tools/openapi-tool.d.ts +17 -0
- package/dist/tools/openapi-tool.d.ts.map +1 -0
- package/dist/tools/openapi-tool.js +165 -0
- package/dist/tools/openapi-tool.js.map +1 -0
- package/dist/trigger-manager.d.ts +26 -0
- package/dist/trigger-manager.d.ts.map +1 -0
- package/dist/trigger-manager.js +107 -0
- package/dist/trigger-manager.js.map +1 -0
- package/dist/webhook.d.ts +95 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +261 -0
- package/dist/webhook.js.map +1 -0
- package/dist/workflow-tools.d.ts +102 -0
- package/dist/workflow-tools.d.ts.map +1 -0
- package/dist/workflow-tools.js +130 -0
- package/dist/workflow-tools.js.map +1 -0
- package/package.json +62 -0
package/dist/engine.js
ADDED
|
@@ -0,0 +1,1453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Execution Engine for marktoflow v2.0
|
|
3
|
+
*
|
|
4
|
+
* Executes workflow steps with retry logic, variable resolution,
|
|
5
|
+
* and SDK invocation.
|
|
6
|
+
*/
|
|
7
|
+
import { StepStatus, WorkflowStatus, createExecutionContext, createStepResult, isActionStep, isSubWorkflowStep, isIfStep, isSwitchStep, isForEachStep, isWhileStep, isMapStep, isFilterStep, isReduceStep, isParallelStep, isTryStep, } from './models.js';
|
|
8
|
+
import { mergePermissions, toSecurityPolicy, } from './permissions.js';
|
|
9
|
+
import { loadPromptFile, resolvePromptTemplate, validatePromptInputs, } from './prompt-loader.js';
|
|
10
|
+
import { DEFAULT_FAILOVER_CONFIG, AgentHealthTracker, FailoverReason, } from './failover.js';
|
|
11
|
+
import { parseFile } from './parser.js';
|
|
12
|
+
import { resolve, dirname } from 'node:path';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Helper Functions
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Convert error to string for display/logging
|
|
18
|
+
*/
|
|
19
|
+
function errorToString(error) {
|
|
20
|
+
if (!error)
|
|
21
|
+
return 'Unknown error';
|
|
22
|
+
if (typeof error === 'string')
|
|
23
|
+
return error;
|
|
24
|
+
if (error instanceof Error)
|
|
25
|
+
return error.message;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.stringify(error);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return String(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Retry Policy
|
|
35
|
+
// ============================================================================
|
|
36
|
+
export class RetryPolicy {
|
|
37
|
+
maxRetries;
|
|
38
|
+
baseDelay;
|
|
39
|
+
maxDelay;
|
|
40
|
+
exponentialBase;
|
|
41
|
+
jitter;
|
|
42
|
+
constructor(maxRetries = 3, baseDelay = 1000, maxDelay = 30000, exponentialBase = 2, jitter = 0.1) {
|
|
43
|
+
this.maxRetries = maxRetries;
|
|
44
|
+
this.baseDelay = baseDelay;
|
|
45
|
+
this.maxDelay = maxDelay;
|
|
46
|
+
this.exponentialBase = exponentialBase;
|
|
47
|
+
this.jitter = jitter;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Calculate delay for a given retry attempt.
|
|
51
|
+
*/
|
|
52
|
+
getDelay(attempt) {
|
|
53
|
+
const exponentialDelay = this.baseDelay * Math.pow(this.exponentialBase, attempt);
|
|
54
|
+
const clampedDelay = Math.min(exponentialDelay, this.maxDelay);
|
|
55
|
+
// Add jitter
|
|
56
|
+
const jitterAmount = clampedDelay * this.jitter * (Math.random() * 2 - 1);
|
|
57
|
+
return Math.max(0, clampedDelay + jitterAmount);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class CircuitBreaker {
|
|
61
|
+
failureThreshold;
|
|
62
|
+
recoveryTimeout;
|
|
63
|
+
halfOpenMaxCalls;
|
|
64
|
+
state = 'CLOSED';
|
|
65
|
+
failures = 0;
|
|
66
|
+
lastFailureTime = 0;
|
|
67
|
+
halfOpenCalls = 0;
|
|
68
|
+
constructor(failureThreshold = 5, recoveryTimeout = 30000, halfOpenMaxCalls = 3) {
|
|
69
|
+
this.failureThreshold = failureThreshold;
|
|
70
|
+
this.recoveryTimeout = recoveryTimeout;
|
|
71
|
+
this.halfOpenMaxCalls = halfOpenMaxCalls;
|
|
72
|
+
}
|
|
73
|
+
canExecute() {
|
|
74
|
+
if (this.state === 'CLOSED') {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (this.state === 'OPEN') {
|
|
78
|
+
const timeSinceFailure = Date.now() - this.lastFailureTime;
|
|
79
|
+
if (timeSinceFailure >= this.recoveryTimeout) {
|
|
80
|
+
this.state = 'HALF_OPEN';
|
|
81
|
+
this.halfOpenCalls = 0;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
// HALF_OPEN
|
|
87
|
+
return this.halfOpenCalls < this.halfOpenMaxCalls;
|
|
88
|
+
}
|
|
89
|
+
recordSuccess() {
|
|
90
|
+
this.failures = 0;
|
|
91
|
+
this.state = 'CLOSED';
|
|
92
|
+
}
|
|
93
|
+
recordFailure() {
|
|
94
|
+
this.failures++;
|
|
95
|
+
this.lastFailureTime = Date.now();
|
|
96
|
+
if (this.state === 'HALF_OPEN') {
|
|
97
|
+
this.state = 'OPEN';
|
|
98
|
+
}
|
|
99
|
+
else if (this.failures >= this.failureThreshold) {
|
|
100
|
+
this.state = 'OPEN';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
getState() {
|
|
104
|
+
return this.state;
|
|
105
|
+
}
|
|
106
|
+
reset() {
|
|
107
|
+
this.state = 'CLOSED';
|
|
108
|
+
this.failures = 0;
|
|
109
|
+
this.halfOpenCalls = 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Variable Resolution
|
|
114
|
+
// ============================================================================
|
|
115
|
+
/**
|
|
116
|
+
* Smart serialization for template interpolation.
|
|
117
|
+
* Converts values to strings in a meaningful way for different types.
|
|
118
|
+
*/
|
|
119
|
+
function serializeValue(value) {
|
|
120
|
+
if (value === undefined || value === null) {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
// For primitives, use standard string conversion
|
|
124
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
125
|
+
return String(value);
|
|
126
|
+
}
|
|
127
|
+
// For objects and arrays, use JSON serialization for readability
|
|
128
|
+
// This makes them useful in prompts and messages instead of "[object Object]"
|
|
129
|
+
try {
|
|
130
|
+
return JSON.stringify(value, null, 2);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// Fallback to string conversion if JSON serialization fails
|
|
134
|
+
return String(value);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolve template variables in a value.
|
|
139
|
+
* Supports {{variable}} and {{inputs.name}} syntax.
|
|
140
|
+
*/
|
|
141
|
+
export function resolveTemplates(value, context) {
|
|
142
|
+
if (typeof value === 'string') {
|
|
143
|
+
// Check if the entire string is a single template expression
|
|
144
|
+
const singleTemplateMatch = value.match(/^\{\{([^}]+)\}\}$/);
|
|
145
|
+
if (singleTemplateMatch) {
|
|
146
|
+
// Return the actual value without converting to string
|
|
147
|
+
const path = singleTemplateMatch[1].trim();
|
|
148
|
+
const resolved = resolveVariablePath(path, context);
|
|
149
|
+
// For single template expressions, return the actual value (could be object, array, etc.)
|
|
150
|
+
// If undefined, return empty string for backward compatibility
|
|
151
|
+
return resolved !== undefined ? resolved : '';
|
|
152
|
+
}
|
|
153
|
+
// Otherwise, do string interpolation with smart serialization
|
|
154
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_, varPath) => {
|
|
155
|
+
const path = varPath.trim();
|
|
156
|
+
const resolved = resolveVariablePath(path, context);
|
|
157
|
+
return serializeValue(resolved);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (Array.isArray(value)) {
|
|
161
|
+
return value.map((v) => resolveTemplates(v, context));
|
|
162
|
+
}
|
|
163
|
+
if (value && typeof value === 'object') {
|
|
164
|
+
const result = {};
|
|
165
|
+
for (const [k, v] of Object.entries(value)) {
|
|
166
|
+
result[k] = resolveTemplates(v, context);
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Resolve a variable path from context.
|
|
174
|
+
* First checks inputs.*, then variables, then stepMetadata, then direct context properties.
|
|
175
|
+
* Exported to allow access from condition evaluation.
|
|
176
|
+
*/
|
|
177
|
+
export function resolveVariablePath(path, context) {
|
|
178
|
+
// Handle inputs.* prefix
|
|
179
|
+
if (path.startsWith('inputs.')) {
|
|
180
|
+
const inputPath = path.slice(7); // Remove 'inputs.'
|
|
181
|
+
return getNestedValue(context.inputs, inputPath);
|
|
182
|
+
}
|
|
183
|
+
// Check variables first (most common case)
|
|
184
|
+
const fromVars = getNestedValue(context.variables, path);
|
|
185
|
+
if (fromVars !== undefined) {
|
|
186
|
+
return fromVars;
|
|
187
|
+
}
|
|
188
|
+
// Check step metadata (for status checks like: step_id.status)
|
|
189
|
+
const fromStepMeta = getNestedValue(context.stepMetadata, path);
|
|
190
|
+
if (fromStepMeta !== undefined) {
|
|
191
|
+
return fromStepMeta;
|
|
192
|
+
}
|
|
193
|
+
// Fall back to direct context access
|
|
194
|
+
return getNestedValue(context, path);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get a nested value from an object using dot notation and array indexing.
|
|
198
|
+
* Supports paths like: "user.name", "items[0].name", "data.users[1].email"
|
|
199
|
+
*/
|
|
200
|
+
function getNestedValue(obj, path) {
|
|
201
|
+
if (obj === null || obj === undefined) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
// Parse path into parts, handling both dot notation and array indexing
|
|
205
|
+
// Convert "a.b[0].c[1]" into ["a", "b", "0", "c", "1"]
|
|
206
|
+
const parts = [];
|
|
207
|
+
let current = '';
|
|
208
|
+
for (let i = 0; i < path.length; i++) {
|
|
209
|
+
const char = path[i];
|
|
210
|
+
if (char === '.') {
|
|
211
|
+
if (current) {
|
|
212
|
+
parts.push(current);
|
|
213
|
+
current = '';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (char === '[') {
|
|
217
|
+
if (current) {
|
|
218
|
+
parts.push(current);
|
|
219
|
+
current = '';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (char === ']') {
|
|
223
|
+
if (current) {
|
|
224
|
+
parts.push(current);
|
|
225
|
+
current = '';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
current += char;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (current) {
|
|
233
|
+
parts.push(current);
|
|
234
|
+
}
|
|
235
|
+
// Traverse the object using the parsed parts
|
|
236
|
+
let result = obj;
|
|
237
|
+
for (const part of parts) {
|
|
238
|
+
if (result === null || result === undefined) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
// Check if part is a number (array index)
|
|
242
|
+
const index = Number(part);
|
|
243
|
+
if (!isNaN(index) && Array.isArray(result)) {
|
|
244
|
+
result = result[index];
|
|
245
|
+
}
|
|
246
|
+
else if (typeof result === 'object') {
|
|
247
|
+
result = result[part];
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
export class WorkflowEngine {
|
|
256
|
+
config;
|
|
257
|
+
retryPolicy;
|
|
258
|
+
circuitBreakers = new Map();
|
|
259
|
+
events;
|
|
260
|
+
stateStore;
|
|
261
|
+
rollbackRegistry;
|
|
262
|
+
failoverConfig;
|
|
263
|
+
healthTracker;
|
|
264
|
+
failoverEvents = [];
|
|
265
|
+
workflowPath; // Base path for resolving sub-workflows
|
|
266
|
+
workflowPermissions; // Workflow-level permissions
|
|
267
|
+
promptCache = new Map(); // Cache for loaded prompts
|
|
268
|
+
constructor(config = {}, events = {}, stateStore) {
|
|
269
|
+
this.config = {
|
|
270
|
+
defaultTimeout: config.defaultTimeout ?? 60000,
|
|
271
|
+
maxRetries: config.maxRetries ?? 3,
|
|
272
|
+
retryBaseDelay: config.retryBaseDelay ?? 1000,
|
|
273
|
+
retryMaxDelay: config.retryMaxDelay ?? 30000,
|
|
274
|
+
defaultAgent: config.defaultAgent,
|
|
275
|
+
defaultModel: config.defaultModel,
|
|
276
|
+
};
|
|
277
|
+
this.retryPolicy = new RetryPolicy(this.config.maxRetries, this.config.retryBaseDelay, this.config.retryMaxDelay);
|
|
278
|
+
this.events = events;
|
|
279
|
+
this.stateStore = stateStore;
|
|
280
|
+
this.rollbackRegistry = config.rollbackRegistry;
|
|
281
|
+
this.failoverConfig = { ...DEFAULT_FAILOVER_CONFIG, ...(config.failoverConfig ?? {}) };
|
|
282
|
+
this.healthTracker = config.healthTracker ?? new AgentHealthTracker();
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Execute a single step - dispatcher to specialized execution methods.
|
|
286
|
+
*/
|
|
287
|
+
async executeStep(step, context, sdkRegistry, stepExecutor) {
|
|
288
|
+
// Check conditions first (applies to all step types)
|
|
289
|
+
if (step.conditions && !this.evaluateConditions(step.conditions, context)) {
|
|
290
|
+
return createStepResult(step.id, StepStatus.SKIPPED, null, new Date());
|
|
291
|
+
}
|
|
292
|
+
// Dispatch to specialized execution method based on step type
|
|
293
|
+
if (isIfStep(step)) {
|
|
294
|
+
return this.executeIfStep(step, context, sdkRegistry, stepExecutor);
|
|
295
|
+
}
|
|
296
|
+
if (isSwitchStep(step)) {
|
|
297
|
+
return this.executeSwitchStep(step, context, sdkRegistry, stepExecutor);
|
|
298
|
+
}
|
|
299
|
+
if (isForEachStep(step)) {
|
|
300
|
+
return this.executeForEachStep(step, context, sdkRegistry, stepExecutor);
|
|
301
|
+
}
|
|
302
|
+
if (isWhileStep(step)) {
|
|
303
|
+
return this.executeWhileStep(step, context, sdkRegistry, stepExecutor);
|
|
304
|
+
}
|
|
305
|
+
if (isMapStep(step)) {
|
|
306
|
+
return this.executeMapStep(step, context, sdkRegistry, stepExecutor);
|
|
307
|
+
}
|
|
308
|
+
if (isFilterStep(step)) {
|
|
309
|
+
return this.executeFilterStep(step, context, sdkRegistry, stepExecutor);
|
|
310
|
+
}
|
|
311
|
+
if (isReduceStep(step)) {
|
|
312
|
+
return this.executeReduceStep(step, context, sdkRegistry, stepExecutor);
|
|
313
|
+
}
|
|
314
|
+
if (isParallelStep(step)) {
|
|
315
|
+
return this.executeParallelStep(step, context, sdkRegistry, stepExecutor);
|
|
316
|
+
}
|
|
317
|
+
if (isTryStep(step)) {
|
|
318
|
+
return this.executeTryStep(step, context, sdkRegistry, stepExecutor);
|
|
319
|
+
}
|
|
320
|
+
// Default: action or workflow step
|
|
321
|
+
return this.executeStepWithFailover(step, context, sdkRegistry, stepExecutor);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Execute a workflow.
|
|
325
|
+
*/
|
|
326
|
+
async execute(workflow, inputs = {}, sdkRegistry, stepExecutor) {
|
|
327
|
+
const context = createExecutionContext(workflow, inputs);
|
|
328
|
+
const stepResults = [];
|
|
329
|
+
const startedAt = new Date();
|
|
330
|
+
// Store workflow-level permissions and defaults
|
|
331
|
+
this.workflowPermissions = workflow.permissions;
|
|
332
|
+
// Use workflow defaults if not set in engine config
|
|
333
|
+
if (!this.config.defaultAgent && workflow.defaultAgent) {
|
|
334
|
+
this.config.defaultAgent = workflow.defaultAgent;
|
|
335
|
+
}
|
|
336
|
+
if (!this.config.defaultModel && workflow.defaultModel) {
|
|
337
|
+
this.config.defaultModel = workflow.defaultModel;
|
|
338
|
+
}
|
|
339
|
+
context.status = WorkflowStatus.RUNNING;
|
|
340
|
+
this.events.onWorkflowStart?.(workflow, context);
|
|
341
|
+
if (this.stateStore) {
|
|
342
|
+
this.stateStore.createExecution({
|
|
343
|
+
runId: context.runId,
|
|
344
|
+
workflowId: workflow.metadata.id,
|
|
345
|
+
workflowPath: 'unknown',
|
|
346
|
+
status: WorkflowStatus.RUNNING,
|
|
347
|
+
startedAt: startedAt,
|
|
348
|
+
completedAt: null,
|
|
349
|
+
currentStep: 0,
|
|
350
|
+
totalSteps: workflow.steps.length,
|
|
351
|
+
inputs: inputs,
|
|
352
|
+
outputs: null,
|
|
353
|
+
error: null,
|
|
354
|
+
metadata: null,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
for (let i = 0; i < workflow.steps.length; i++) {
|
|
359
|
+
const step = workflow.steps[i];
|
|
360
|
+
context.currentStepIndex = i;
|
|
361
|
+
// Execute step using dispatcher
|
|
362
|
+
const result = await this.executeStep(step, context, sdkRegistry, stepExecutor);
|
|
363
|
+
stepResults.push(result);
|
|
364
|
+
// Store step metadata (status, error, etc.) in separate field for condition evaluation
|
|
365
|
+
// This allows conditions like: step_id.status == 'failed'
|
|
366
|
+
context.stepMetadata[step.id] = {
|
|
367
|
+
status: result.status.toLowerCase(),
|
|
368
|
+
retryCount: result.retryCount,
|
|
369
|
+
...(result.error ? { error: errorToString(result.error) } : {}),
|
|
370
|
+
};
|
|
371
|
+
// Store output variable
|
|
372
|
+
if (step.outputVariable && result.status === StepStatus.COMPLETED) {
|
|
373
|
+
context.variables[step.outputVariable] = result.output;
|
|
374
|
+
}
|
|
375
|
+
// Check if this step set workflow outputs (from workflow.set_outputs action)
|
|
376
|
+
if (result.status === StepStatus.COMPLETED &&
|
|
377
|
+
result.output &&
|
|
378
|
+
typeof result.output === 'object' &&
|
|
379
|
+
'__workflow_outputs__' in result.output) {
|
|
380
|
+
const outputObj = result.output;
|
|
381
|
+
const outputs = outputObj['__workflow_outputs__'];
|
|
382
|
+
context.workflowOutputs = outputs;
|
|
383
|
+
}
|
|
384
|
+
// Handle failure
|
|
385
|
+
if (result.status === StepStatus.FAILED) {
|
|
386
|
+
// Get error action from step if it has error handling
|
|
387
|
+
let errorAction = 'stop';
|
|
388
|
+
if ('errorHandling' in step && step.errorHandling?.action) {
|
|
389
|
+
errorAction = step.errorHandling.action;
|
|
390
|
+
}
|
|
391
|
+
if (errorAction === 'stop') {
|
|
392
|
+
context.status = WorkflowStatus.FAILED;
|
|
393
|
+
const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
|
|
394
|
+
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
|
|
395
|
+
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
396
|
+
return workflowResult;
|
|
397
|
+
}
|
|
398
|
+
// 'continue' - keep going
|
|
399
|
+
if (errorAction === 'rollback') {
|
|
400
|
+
if (this.rollbackRegistry) {
|
|
401
|
+
await this.rollbackRegistry.rollbackAllAsync({
|
|
402
|
+
context,
|
|
403
|
+
inputs: context.inputs,
|
|
404
|
+
variables: context.variables,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
context.status = WorkflowStatus.FAILED;
|
|
408
|
+
const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
|
|
409
|
+
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
|
|
410
|
+
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
411
|
+
return workflowResult;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Determine final status
|
|
416
|
+
context.status = WorkflowStatus.COMPLETED;
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
context.status = WorkflowStatus.FAILED;
|
|
420
|
+
if (this.stateStore) {
|
|
421
|
+
this.stateStore.updateExecution(context.runId, {
|
|
422
|
+
status: WorkflowStatus.FAILED,
|
|
423
|
+
completedAt: new Date(),
|
|
424
|
+
error: error instanceof Error ? error.message : String(error),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, error instanceof Error ? error.message : String(error));
|
|
428
|
+
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
429
|
+
return workflowResult;
|
|
430
|
+
}
|
|
431
|
+
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt);
|
|
432
|
+
if (this.stateStore) {
|
|
433
|
+
this.stateStore.updateExecution(context.runId, {
|
|
434
|
+
status: context.status,
|
|
435
|
+
completedAt: new Date(),
|
|
436
|
+
outputs: context.variables,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
440
|
+
return workflowResult;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Execute a workflow from a file.
|
|
444
|
+
* This method automatically sets the workflow path for resolving sub-workflows.
|
|
445
|
+
*/
|
|
446
|
+
async executeFile(workflowPath, inputs = {}, sdkRegistry, stepExecutor) {
|
|
447
|
+
// Parse the workflow file
|
|
448
|
+
const { workflow } = await parseFile(workflowPath);
|
|
449
|
+
// Set the workflow path for sub-workflow resolution
|
|
450
|
+
this.workflowPath = resolve(workflowPath);
|
|
451
|
+
// Execute the workflow
|
|
452
|
+
return this.execute(workflow, inputs, sdkRegistry, stepExecutor);
|
|
453
|
+
}
|
|
454
|
+
getFailoverHistory() {
|
|
455
|
+
return [...this.failoverEvents];
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Execute a sub-workflow.
|
|
459
|
+
*/
|
|
460
|
+
async executeSubWorkflow(step, context, sdkRegistry, stepExecutor) {
|
|
461
|
+
if (!isSubWorkflowStep(step)) {
|
|
462
|
+
throw new Error(`Step ${step.id} is not a workflow step`);
|
|
463
|
+
}
|
|
464
|
+
// Resolve the sub-workflow path relative to the parent workflow
|
|
465
|
+
const subWorkflowPath = this.workflowPath
|
|
466
|
+
? resolve(dirname(this.workflowPath), step.workflow)
|
|
467
|
+
: resolve(step.workflow);
|
|
468
|
+
// Parse the sub-workflow
|
|
469
|
+
const { workflow: subWorkflow } = await parseFile(subWorkflowPath);
|
|
470
|
+
// Resolve inputs for the sub-workflow
|
|
471
|
+
const resolvedInputs = resolveTemplates(step.inputs, context);
|
|
472
|
+
// Create a new engine instance for the sub-workflow with the same configuration
|
|
473
|
+
const subEngineConfig = {
|
|
474
|
+
defaultTimeout: this.config.defaultTimeout,
|
|
475
|
+
maxRetries: this.config.maxRetries,
|
|
476
|
+
retryBaseDelay: this.config.retryBaseDelay,
|
|
477
|
+
retryMaxDelay: this.config.retryMaxDelay,
|
|
478
|
+
failoverConfig: this.failoverConfig,
|
|
479
|
+
healthTracker: this.healthTracker,
|
|
480
|
+
};
|
|
481
|
+
if (this.rollbackRegistry) {
|
|
482
|
+
subEngineConfig.rollbackRegistry = this.rollbackRegistry;
|
|
483
|
+
}
|
|
484
|
+
const subEngine = new WorkflowEngine(subEngineConfig, this.events, this.stateStore);
|
|
485
|
+
// Set the base path for the sub-workflow
|
|
486
|
+
subEngine.workflowPath = subWorkflowPath;
|
|
487
|
+
// Execute the sub-workflow
|
|
488
|
+
const result = await subEngine.execute(subWorkflow, resolvedInputs, sdkRegistry, stepExecutor);
|
|
489
|
+
// Check if sub-workflow failed
|
|
490
|
+
if (result.status === WorkflowStatus.FAILED) {
|
|
491
|
+
throw new Error(result.error || 'Sub-workflow execution failed');
|
|
492
|
+
}
|
|
493
|
+
// Return the sub-workflow output
|
|
494
|
+
return result.output;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Execute a sub-workflow using an AI sub-agent.
|
|
498
|
+
* The agent interprets the workflow and executes it autonomously.
|
|
499
|
+
*/
|
|
500
|
+
async executeSubWorkflowWithAgent(step, context, sdkRegistry, stepExecutor) {
|
|
501
|
+
// Resolve the sub-workflow path
|
|
502
|
+
const subWorkflowPath = this.workflowPath
|
|
503
|
+
? resolve(dirname(this.workflowPath), step.workflow)
|
|
504
|
+
: resolve(step.workflow);
|
|
505
|
+
// Read the workflow file content
|
|
506
|
+
const { readFile } = await import('node:fs/promises');
|
|
507
|
+
const workflowContent = await readFile(subWorkflowPath, 'utf-8');
|
|
508
|
+
// Resolve inputs for the sub-workflow
|
|
509
|
+
const resolvedInputs = resolveTemplates(step.inputs, context);
|
|
510
|
+
// Get subagent configuration
|
|
511
|
+
const subagentConfig = step.subagentConfig || {};
|
|
512
|
+
const model = subagentConfig.model || step.model || this.config.defaultModel;
|
|
513
|
+
const maxTurns = subagentConfig.maxTurns || 10;
|
|
514
|
+
const systemPrompt = subagentConfig.systemPrompt || this.buildDefaultSubagentSystemPrompt();
|
|
515
|
+
const tools = subagentConfig.tools || ['Read', 'Write', 'Bash', 'Glob', 'Grep'];
|
|
516
|
+
// Build the prompt for the agent
|
|
517
|
+
const agentPrompt = this.buildSubagentPrompt(workflowContent, resolvedInputs, tools);
|
|
518
|
+
// Determine the agent action to use
|
|
519
|
+
const agentName = step.agent || this.config.defaultAgent || 'agent';
|
|
520
|
+
const agentAction = `${agentName}.chat.completions`;
|
|
521
|
+
// Build the messages array
|
|
522
|
+
const messages = [
|
|
523
|
+
{ role: 'system', content: systemPrompt },
|
|
524
|
+
{ role: 'user', content: agentPrompt },
|
|
525
|
+
];
|
|
526
|
+
// Create a virtual action step to execute via the agent
|
|
527
|
+
const agentStep = {
|
|
528
|
+
id: `${step.id}-subagent`,
|
|
529
|
+
type: 'action',
|
|
530
|
+
action: agentAction,
|
|
531
|
+
inputs: {
|
|
532
|
+
model,
|
|
533
|
+
messages,
|
|
534
|
+
max_tokens: 8192,
|
|
535
|
+
},
|
|
536
|
+
model,
|
|
537
|
+
agent: agentName,
|
|
538
|
+
};
|
|
539
|
+
// Build executor context
|
|
540
|
+
const executorContext = this.buildStepExecutorContext(agentStep);
|
|
541
|
+
// Execute the agent call
|
|
542
|
+
let response;
|
|
543
|
+
let turns = 0;
|
|
544
|
+
let conversationMessages = [...messages];
|
|
545
|
+
let finalOutput = {};
|
|
546
|
+
while (turns < maxTurns) {
|
|
547
|
+
turns++;
|
|
548
|
+
try {
|
|
549
|
+
response = await stepExecutor({ ...agentStep, inputs: { ...agentStep.inputs, messages: conversationMessages } }, context, sdkRegistry, executorContext);
|
|
550
|
+
// Parse the response
|
|
551
|
+
const parsedResponse = this.parseSubagentResponse(response);
|
|
552
|
+
if (parsedResponse.completed) {
|
|
553
|
+
finalOutput = parsedResponse.output || {};
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
// If agent needs to continue, add its response and continue
|
|
557
|
+
if (parsedResponse.message) {
|
|
558
|
+
conversationMessages.push({ role: 'assistant', content: parsedResponse.message });
|
|
559
|
+
// Agent might request a tool call - for now, we'll prompt it to continue
|
|
560
|
+
conversationMessages.push({ role: 'user', content: 'Continue with the workflow execution.' });
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
// No clear continuation, assume completed
|
|
564
|
+
finalOutput = parsedResponse.output || {};
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
throw new Error(`Sub-agent execution failed at turn ${turns}: ${error instanceof Error ? error.message : String(error)}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (turns >= maxTurns) {
|
|
573
|
+
throw new Error(`Sub-agent exceeded maximum turns (${maxTurns})`);
|
|
574
|
+
}
|
|
575
|
+
return finalOutput;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Build the default system prompt for sub-agent execution.
|
|
579
|
+
*/
|
|
580
|
+
buildDefaultSubagentSystemPrompt() {
|
|
581
|
+
return `You are an AI agent executing a workflow. Your task is to interpret the workflow definition and execute each step in order.
|
|
582
|
+
|
|
583
|
+
For each step:
|
|
584
|
+
1. Understand what the step requires
|
|
585
|
+
2. Execute the action described
|
|
586
|
+
3. Store any outputs as specified
|
|
587
|
+
|
|
588
|
+
When you complete all steps, respond with a JSON object containing the workflow outputs.
|
|
589
|
+
|
|
590
|
+
Format your final response as:
|
|
591
|
+
\`\`\`json
|
|
592
|
+
{
|
|
593
|
+
"completed": true,
|
|
594
|
+
"output": { /* workflow outputs here */ }
|
|
595
|
+
}
|
|
596
|
+
\`\`\`
|
|
597
|
+
|
|
598
|
+
If you encounter an error, respond with:
|
|
599
|
+
\`\`\`json
|
|
600
|
+
{
|
|
601
|
+
"completed": false,
|
|
602
|
+
"error": "description of the error"
|
|
603
|
+
}
|
|
604
|
+
\`\`\``;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Build the prompt for sub-agent workflow execution.
|
|
608
|
+
*/
|
|
609
|
+
buildSubagentPrompt(workflowContent, inputs, tools) {
|
|
610
|
+
return `Execute the following workflow:
|
|
611
|
+
|
|
612
|
+
## Workflow Definition
|
|
613
|
+
\`\`\`markdown
|
|
614
|
+
${workflowContent}
|
|
615
|
+
\`\`\`
|
|
616
|
+
|
|
617
|
+
## Inputs
|
|
618
|
+
\`\`\`json
|
|
619
|
+
${JSON.stringify(inputs, null, 2)}
|
|
620
|
+
\`\`\`
|
|
621
|
+
|
|
622
|
+
## Available Tools
|
|
623
|
+
${tools.join(', ')}
|
|
624
|
+
|
|
625
|
+
Execute the workflow steps in order and return the final outputs as JSON.`;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Parse the sub-agent's response to extract completion status and output.
|
|
629
|
+
*/
|
|
630
|
+
parseSubagentResponse(response) {
|
|
631
|
+
// Try to extract content from various response formats
|
|
632
|
+
let content;
|
|
633
|
+
if (typeof response === 'string') {
|
|
634
|
+
content = response;
|
|
635
|
+
}
|
|
636
|
+
else if (response && typeof response === 'object') {
|
|
637
|
+
const resp = response;
|
|
638
|
+
// OpenAI-style response
|
|
639
|
+
if (resp.choices && Array.isArray(resp.choices)) {
|
|
640
|
+
const choice = resp.choices[0];
|
|
641
|
+
if (choice.message && typeof choice.message === 'object') {
|
|
642
|
+
const message = choice.message;
|
|
643
|
+
content = message.content;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Anthropic-style response
|
|
647
|
+
else if (resp.content && Array.isArray(resp.content)) {
|
|
648
|
+
const textBlock = resp.content.find((c) => typeof c === 'object' && c !== null && c.type === 'text');
|
|
649
|
+
content = textBlock?.text;
|
|
650
|
+
}
|
|
651
|
+
// Direct content field
|
|
652
|
+
else if (typeof resp.content === 'string') {
|
|
653
|
+
content = resp.content;
|
|
654
|
+
}
|
|
655
|
+
// Direct message field
|
|
656
|
+
else if (typeof resp.message === 'string') {
|
|
657
|
+
content = resp.message;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (!content) {
|
|
661
|
+
return { completed: false, message: 'No content in response' };
|
|
662
|
+
}
|
|
663
|
+
// Try to parse JSON from the response
|
|
664
|
+
const jsonMatch = content.match(/```json\n?([\s\S]*?)```/);
|
|
665
|
+
if (jsonMatch) {
|
|
666
|
+
try {
|
|
667
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
668
|
+
const output = parsed.output;
|
|
669
|
+
const error = parsed.error;
|
|
670
|
+
return {
|
|
671
|
+
completed: parsed.completed === true,
|
|
672
|
+
...(output !== undefined ? { output } : {}),
|
|
673
|
+
...(error !== undefined ? { error } : {}),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
// JSON parse failed, treat as message
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// Try to parse raw JSON
|
|
681
|
+
try {
|
|
682
|
+
const parsed = JSON.parse(content);
|
|
683
|
+
if (typeof parsed.completed === 'boolean') {
|
|
684
|
+
const output = parsed.output;
|
|
685
|
+
const error = parsed.error;
|
|
686
|
+
return {
|
|
687
|
+
completed: parsed.completed,
|
|
688
|
+
...(output !== undefined ? { output } : {}),
|
|
689
|
+
...(error !== undefined ? { error } : {}),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
// Not JSON
|
|
695
|
+
}
|
|
696
|
+
// Return the content as a message
|
|
697
|
+
return { completed: false, message: content };
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Build the step executor context with effective model/agent/permissions.
|
|
701
|
+
*/
|
|
702
|
+
buildStepExecutorContext(step) {
|
|
703
|
+
// Merge workflow and step permissions
|
|
704
|
+
const effectivePermissions = mergePermissions(this.workflowPermissions, step.permissions);
|
|
705
|
+
// Resolve effective model/agent (step overrides workflow defaults)
|
|
706
|
+
const effectiveModel = step.model || this.config.defaultModel;
|
|
707
|
+
const effectiveAgent = step.agent || this.config.defaultAgent;
|
|
708
|
+
return {
|
|
709
|
+
model: effectiveModel,
|
|
710
|
+
agent: effectiveAgent,
|
|
711
|
+
permissions: effectivePermissions,
|
|
712
|
+
securityPolicy: toSecurityPolicy(effectivePermissions),
|
|
713
|
+
basePath: this.workflowPath,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Load and resolve an external prompt file for a step.
|
|
718
|
+
*/
|
|
719
|
+
async loadAndResolvePrompt(step, context) {
|
|
720
|
+
if (!step.prompt) {
|
|
721
|
+
return step.inputs;
|
|
722
|
+
}
|
|
723
|
+
// Check cache
|
|
724
|
+
let loadedPrompt = this.promptCache.get(step.prompt);
|
|
725
|
+
if (!loadedPrompt) {
|
|
726
|
+
loadedPrompt = await loadPromptFile(step.prompt, this.workflowPath);
|
|
727
|
+
this.promptCache.set(step.prompt, loadedPrompt);
|
|
728
|
+
}
|
|
729
|
+
// Resolve prompt inputs (from step.promptInputs, with template resolution)
|
|
730
|
+
const promptInputs = step.promptInputs
|
|
731
|
+
? resolveTemplates(step.promptInputs, context)
|
|
732
|
+
: {};
|
|
733
|
+
// Validate prompt inputs
|
|
734
|
+
const validation = validatePromptInputs(loadedPrompt, promptInputs);
|
|
735
|
+
if (!validation.valid) {
|
|
736
|
+
throw new Error(`Invalid prompt inputs: ${validation.errors.join(', ')}`);
|
|
737
|
+
}
|
|
738
|
+
// Resolve the prompt template
|
|
739
|
+
const resolved = resolvePromptTemplate(loadedPrompt, promptInputs, context);
|
|
740
|
+
// Merge resolved prompt content into inputs
|
|
741
|
+
// The resolved content typically goes into a 'messages' or 'prompt' field
|
|
742
|
+
const resolvedInputs = { ...step.inputs };
|
|
743
|
+
// If inputs has a 'messages' array with a user message, inject prompt content
|
|
744
|
+
if (Array.isArray(resolvedInputs.messages)) {
|
|
745
|
+
resolvedInputs.messages = resolvedInputs.messages.map((msg) => {
|
|
746
|
+
if (typeof msg === 'object' && msg !== null) {
|
|
747
|
+
const message = msg;
|
|
748
|
+
if (message.role === 'user' && typeof message.content === 'string') {
|
|
749
|
+
// Replace {{ prompt }} placeholder with resolved content
|
|
750
|
+
return {
|
|
751
|
+
...message,
|
|
752
|
+
content: message.content.replace(/\{\{\s*prompt\s*\}\}/g, resolved.content),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return msg;
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
// Add resolved prompt as 'promptContent' for the executor to use
|
|
761
|
+
resolvedInputs.promptContent = resolved.content;
|
|
762
|
+
}
|
|
763
|
+
return resolvedInputs;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Execute a step with retry logic.
|
|
767
|
+
*/
|
|
768
|
+
async executeStepWithRetry(step, context, sdkRegistry, stepExecutor) {
|
|
769
|
+
const startedAt = new Date();
|
|
770
|
+
let lastError;
|
|
771
|
+
// Build executor context with model/agent/permissions
|
|
772
|
+
const executorContext = this.buildStepExecutorContext(step);
|
|
773
|
+
// Handle sub-workflow execution
|
|
774
|
+
if (isSubWorkflowStep(step)) {
|
|
775
|
+
// Check if we should use subagent execution
|
|
776
|
+
if (step.useSubagent) {
|
|
777
|
+
try {
|
|
778
|
+
this.events.onStepStart?.(step, context);
|
|
779
|
+
const output = await this.executeWithTimeout(() => this.executeSubWorkflowWithAgent(step, context, sdkRegistry, stepExecutor), step.timeout ?? this.config.defaultTimeout);
|
|
780
|
+
const result = createStepResult(step.id, StepStatus.COMPLETED, output, startedAt, 0);
|
|
781
|
+
this.events.onStepComplete?.(step, result);
|
|
782
|
+
return result;
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
786
|
+
const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, lastError);
|
|
787
|
+
this.events.onStepComplete?.(step, result);
|
|
788
|
+
return result;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// Standard sub-workflow execution
|
|
792
|
+
try {
|
|
793
|
+
this.events.onStepStart?.(step, context);
|
|
794
|
+
const output = await this.executeWithTimeout(() => this.executeSubWorkflow(step, context, sdkRegistry, stepExecutor), step.timeout ?? this.config.defaultTimeout);
|
|
795
|
+
const result = createStepResult(step.id, StepStatus.COMPLETED, output, startedAt, 0);
|
|
796
|
+
this.events.onStepComplete?.(step, result);
|
|
797
|
+
return result;
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
801
|
+
const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, lastError // Pass full error object
|
|
802
|
+
);
|
|
803
|
+
this.events.onStepComplete?.(step, result);
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
// Regular action step - ensure action is defined
|
|
808
|
+
if (!isActionStep(step)) {
|
|
809
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Step is neither an action nor a workflow');
|
|
810
|
+
}
|
|
811
|
+
const maxRetries = step.errorHandling?.maxRetries ?? this.config.maxRetries;
|
|
812
|
+
// Get or create circuit breaker for this step's action
|
|
813
|
+
const [serviceName] = step.action.split('.');
|
|
814
|
+
let circuitBreaker = this.circuitBreakers.get(serviceName);
|
|
815
|
+
if (!circuitBreaker) {
|
|
816
|
+
circuitBreaker = new CircuitBreaker();
|
|
817
|
+
this.circuitBreakers.set(serviceName, circuitBreaker);
|
|
818
|
+
}
|
|
819
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
820
|
+
// Check circuit breaker
|
|
821
|
+
if (!circuitBreaker.canExecute()) {
|
|
822
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, attempt, `Circuit breaker open for service: ${serviceName}`);
|
|
823
|
+
}
|
|
824
|
+
this.events.onStepStart?.(step, context);
|
|
825
|
+
try {
|
|
826
|
+
// Load and resolve external prompt if specified
|
|
827
|
+
let resolvedInputs;
|
|
828
|
+
if (step.prompt) {
|
|
829
|
+
resolvedInputs = await this.loadAndResolvePrompt(step, context);
|
|
830
|
+
resolvedInputs = resolveTemplates(resolvedInputs, context);
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
// Resolve templates in inputs
|
|
834
|
+
resolvedInputs = resolveTemplates(step.inputs, context);
|
|
835
|
+
}
|
|
836
|
+
const stepWithResolvedInputs = { ...step, inputs: resolvedInputs };
|
|
837
|
+
// Execute step with executor context
|
|
838
|
+
const output = await this.executeWithTimeout(() => stepExecutor(stepWithResolvedInputs, context, sdkRegistry, executorContext), step.timeout ?? this.config.defaultTimeout);
|
|
839
|
+
circuitBreaker.recordSuccess();
|
|
840
|
+
const result = createStepResult(step.id, StepStatus.COMPLETED, output, startedAt, attempt);
|
|
841
|
+
this.events.onStepComplete?.(step, result);
|
|
842
|
+
return result;
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
846
|
+
circuitBreaker.recordFailure();
|
|
847
|
+
this.events.onStepError?.(step, lastError, attempt);
|
|
848
|
+
// Wait before retry (unless last attempt)
|
|
849
|
+
if (attempt < maxRetries) {
|
|
850
|
+
const delay = this.retryPolicy.getDelay(attempt);
|
|
851
|
+
await sleep(delay);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// All retries exhausted
|
|
856
|
+
const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, maxRetries, lastError // Pass full error object to preserve HTTP details, stack traces, etc.
|
|
857
|
+
);
|
|
858
|
+
this.events.onStepComplete?.(step, result);
|
|
859
|
+
return result;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Execute a step with retry + failover support.
|
|
863
|
+
*/
|
|
864
|
+
async executeStepWithFailover(step, context, sdkRegistry, stepExecutor) {
|
|
865
|
+
const primaryResult = await this.executeStepWithRetry(step, context, sdkRegistry, stepExecutor);
|
|
866
|
+
// Sub-workflows and non-action steps don't support failover
|
|
867
|
+
if (!isActionStep(step)) {
|
|
868
|
+
return primaryResult;
|
|
869
|
+
}
|
|
870
|
+
const [primaryTool, ...methodParts] = step.action.split('.');
|
|
871
|
+
const method = methodParts.join('.');
|
|
872
|
+
if (primaryResult.status === StepStatus.COMPLETED) {
|
|
873
|
+
this.healthTracker.markHealthy(primaryTool);
|
|
874
|
+
return primaryResult;
|
|
875
|
+
}
|
|
876
|
+
const errorMessage = primaryResult.error ? errorToString(primaryResult.error) : '';
|
|
877
|
+
const isTimeout = errorMessage.includes('timed out');
|
|
878
|
+
if (isTimeout && !this.failoverConfig.failoverOnTimeout) {
|
|
879
|
+
this.healthTracker.markUnhealthy(primaryTool, errorMessage);
|
|
880
|
+
return primaryResult;
|
|
881
|
+
}
|
|
882
|
+
if (!isTimeout && !this.failoverConfig.failoverOnStepFailure) {
|
|
883
|
+
this.healthTracker.markUnhealthy(primaryTool, errorMessage);
|
|
884
|
+
return primaryResult;
|
|
885
|
+
}
|
|
886
|
+
if (!method || this.failoverConfig.fallbackAgents.length === 0) {
|
|
887
|
+
this.healthTracker.markUnhealthy(primaryTool, errorMessage);
|
|
888
|
+
return primaryResult;
|
|
889
|
+
}
|
|
890
|
+
let attempts = 0;
|
|
891
|
+
for (const fallbackTool of this.failoverConfig.fallbackAgents) {
|
|
892
|
+
if (fallbackTool === primaryTool)
|
|
893
|
+
continue;
|
|
894
|
+
if (attempts >= this.failoverConfig.maxFailoverAttempts)
|
|
895
|
+
break;
|
|
896
|
+
const fallbackStep = { ...step, action: `${fallbackTool}.${method}`, type: 'action' };
|
|
897
|
+
const result = await this.executeStepWithRetry(fallbackStep, context, sdkRegistry, stepExecutor);
|
|
898
|
+
this.failoverEvents.push({
|
|
899
|
+
timestamp: new Date(),
|
|
900
|
+
fromAgent: primaryTool,
|
|
901
|
+
toAgent: fallbackTool,
|
|
902
|
+
reason: isTimeout ? FailoverReason.TIMEOUT : FailoverReason.STEP_EXECUTION_FAILED,
|
|
903
|
+
stepIndex: context.currentStepIndex,
|
|
904
|
+
error: errorMessage || undefined,
|
|
905
|
+
});
|
|
906
|
+
attempts += 1;
|
|
907
|
+
if (result.status === StepStatus.COMPLETED) {
|
|
908
|
+
this.healthTracker.markHealthy(fallbackTool);
|
|
909
|
+
return result;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
this.healthTracker.markUnhealthy(primaryTool, errorMessage);
|
|
913
|
+
return primaryResult;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Execute a function with a timeout.
|
|
917
|
+
*/
|
|
918
|
+
async executeWithTimeout(fn, timeoutMs) {
|
|
919
|
+
return Promise.race([
|
|
920
|
+
fn(),
|
|
921
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Step timed out after ${timeoutMs}ms`)), timeoutMs)),
|
|
922
|
+
]);
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Evaluate step conditions.
|
|
926
|
+
*/
|
|
927
|
+
evaluateConditions(conditions, context) {
|
|
928
|
+
for (const condition of conditions) {
|
|
929
|
+
if (!this.evaluateCondition(condition, context)) {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Evaluate a single condition.
|
|
937
|
+
* Supports: ==, !=, >, <, >=, <=
|
|
938
|
+
* Also supports nested property access (e.g., check_result.success)
|
|
939
|
+
* and step status checks (e.g., step_id.status == 'failed')
|
|
940
|
+
*/
|
|
941
|
+
evaluateCondition(condition, context) {
|
|
942
|
+
// Simple expression parsing
|
|
943
|
+
const operators = ['==', '!=', '>=', '<=', '>', '<'];
|
|
944
|
+
let operator;
|
|
945
|
+
let parts = [];
|
|
946
|
+
for (const op of operators) {
|
|
947
|
+
if (condition.includes(op)) {
|
|
948
|
+
operator = op;
|
|
949
|
+
parts = condition.split(op).map((s) => s.trim());
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (!operator || parts.length !== 2) {
|
|
954
|
+
// Treat as boolean variable reference with nested property support
|
|
955
|
+
const value = this.resolveConditionValue(condition, context);
|
|
956
|
+
return Boolean(value);
|
|
957
|
+
}
|
|
958
|
+
const left = this.resolveConditionValue(parts[0], context);
|
|
959
|
+
const right = this.parseValue(parts[1]);
|
|
960
|
+
switch (operator) {
|
|
961
|
+
case '==':
|
|
962
|
+
return left == right;
|
|
963
|
+
case '!=':
|
|
964
|
+
return left != right;
|
|
965
|
+
case '>':
|
|
966
|
+
return Number(left) > Number(right);
|
|
967
|
+
case '<':
|
|
968
|
+
return Number(left) < Number(right);
|
|
969
|
+
case '>=':
|
|
970
|
+
return Number(left) >= Number(right);
|
|
971
|
+
case '<=':
|
|
972
|
+
return Number(left) <= Number(right);
|
|
973
|
+
default:
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Resolve a condition value with support for nested properties.
|
|
979
|
+
* Handles direct variable references and nested paths.
|
|
980
|
+
*/
|
|
981
|
+
resolveConditionValue(path, context) {
|
|
982
|
+
// First try to parse as a literal value (true, false, numbers, etc.)
|
|
983
|
+
const parsedValue = this.parseValue(path);
|
|
984
|
+
// If parseValue returned the same string, try to resolve as a variable
|
|
985
|
+
if (parsedValue === path) {
|
|
986
|
+
const resolved = resolveVariablePath(path, context);
|
|
987
|
+
return resolved;
|
|
988
|
+
}
|
|
989
|
+
// Return the parsed literal value
|
|
990
|
+
return parsedValue;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Parse a value from a condition string.
|
|
994
|
+
*/
|
|
995
|
+
parseValue(value) {
|
|
996
|
+
// Remove quotes
|
|
997
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
998
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
999
|
+
return value.slice(1, -1);
|
|
1000
|
+
}
|
|
1001
|
+
// Numbers
|
|
1002
|
+
if (!isNaN(Number(value))) {
|
|
1003
|
+
return Number(value);
|
|
1004
|
+
}
|
|
1005
|
+
// Booleans
|
|
1006
|
+
if (value === 'true')
|
|
1007
|
+
return true;
|
|
1008
|
+
if (value === 'false')
|
|
1009
|
+
return false;
|
|
1010
|
+
if (value === 'null')
|
|
1011
|
+
return null;
|
|
1012
|
+
return value;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Build the final workflow result.
|
|
1016
|
+
*/
|
|
1017
|
+
buildWorkflowResult(workflow, context, stepResults, startedAt, error) {
|
|
1018
|
+
const completedAt = new Date();
|
|
1019
|
+
// Use workflowOutputs if set by workflow.set_outputs, otherwise use all variables
|
|
1020
|
+
const output = context.workflowOutputs || context.variables;
|
|
1021
|
+
return {
|
|
1022
|
+
workflowId: workflow.metadata.id,
|
|
1023
|
+
runId: context.runId,
|
|
1024
|
+
status: context.status,
|
|
1025
|
+
stepResults,
|
|
1026
|
+
output,
|
|
1027
|
+
error,
|
|
1028
|
+
startedAt,
|
|
1029
|
+
completedAt,
|
|
1030
|
+
duration: completedAt.getTime() - startedAt.getTime(),
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Reset all circuit breakers.
|
|
1035
|
+
*/
|
|
1036
|
+
resetCircuitBreakers() {
|
|
1037
|
+
for (const breaker of this.circuitBreakers.values()) {
|
|
1038
|
+
breaker.reset();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
// ============================================================================
|
|
1042
|
+
// Control Flow Execution Methods
|
|
1043
|
+
// ============================================================================
|
|
1044
|
+
/**
|
|
1045
|
+
* Execute an if/else conditional step.
|
|
1046
|
+
*/
|
|
1047
|
+
async executeIfStep(step, context, sdkRegistry, stepExecutor) {
|
|
1048
|
+
const startedAt = new Date();
|
|
1049
|
+
try {
|
|
1050
|
+
// Evaluate condition
|
|
1051
|
+
const conditionResult = this.evaluateCondition(step.condition, context);
|
|
1052
|
+
// Determine which branch to execute
|
|
1053
|
+
const branchSteps = conditionResult
|
|
1054
|
+
? step.then || step.steps // 'steps' is alias for 'then'
|
|
1055
|
+
: step.else;
|
|
1056
|
+
if (!branchSteps || branchSteps.length === 0) {
|
|
1057
|
+
return createStepResult(step.id, StepStatus.SKIPPED, null, startedAt);
|
|
1058
|
+
}
|
|
1059
|
+
// Execute the branch steps
|
|
1060
|
+
const branchResults = [];
|
|
1061
|
+
for (const branchStep of branchSteps) {
|
|
1062
|
+
const result = await this.executeStep(branchStep, context, sdkRegistry, stepExecutor);
|
|
1063
|
+
if (result.status === StepStatus.COMPLETED && branchStep.outputVariable) {
|
|
1064
|
+
context.variables[branchStep.outputVariable] = result.output;
|
|
1065
|
+
branchResults.push(result.output);
|
|
1066
|
+
}
|
|
1067
|
+
if (result.status === StepStatus.FAILED) {
|
|
1068
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return createStepResult(step.id, StepStatus.COMPLETED, branchResults, startedAt);
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Execute a switch/case step.
|
|
1079
|
+
*/
|
|
1080
|
+
async executeSwitchStep(step, context, sdkRegistry, stepExecutor) {
|
|
1081
|
+
const startedAt = new Date();
|
|
1082
|
+
try {
|
|
1083
|
+
// Resolve the switch expression
|
|
1084
|
+
const expressionValue = String(resolveTemplates(step.expression, context));
|
|
1085
|
+
// Find matching case
|
|
1086
|
+
const caseSteps = step.cases[expressionValue] || step.default;
|
|
1087
|
+
if (!caseSteps || caseSteps.length === 0) {
|
|
1088
|
+
return createStepResult(step.id, StepStatus.SKIPPED, null, startedAt);
|
|
1089
|
+
}
|
|
1090
|
+
// Execute case steps
|
|
1091
|
+
const caseResults = [];
|
|
1092
|
+
for (const caseStep of caseSteps) {
|
|
1093
|
+
const result = await this.executeStep(caseStep, context, sdkRegistry, stepExecutor);
|
|
1094
|
+
if (result.status === StepStatus.COMPLETED && caseStep.outputVariable) {
|
|
1095
|
+
context.variables[caseStep.outputVariable] = result.output;
|
|
1096
|
+
caseResults.push(result.output);
|
|
1097
|
+
}
|
|
1098
|
+
if (result.status === StepStatus.FAILED) {
|
|
1099
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return createStepResult(step.id, StepStatus.COMPLETED, caseResults, startedAt);
|
|
1103
|
+
}
|
|
1104
|
+
catch (error) {
|
|
1105
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Execute a for-each loop step.
|
|
1110
|
+
*/
|
|
1111
|
+
async executeForEachStep(step, context, sdkRegistry, stepExecutor) {
|
|
1112
|
+
const startedAt = new Date();
|
|
1113
|
+
try {
|
|
1114
|
+
// Resolve items array
|
|
1115
|
+
const items = resolveTemplates(step.items, context);
|
|
1116
|
+
if (!Array.isArray(items)) {
|
|
1117
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
|
|
1118
|
+
}
|
|
1119
|
+
if (items.length === 0) {
|
|
1120
|
+
return createStepResult(step.id, StepStatus.SKIPPED, [], startedAt);
|
|
1121
|
+
}
|
|
1122
|
+
// Execute steps for each item
|
|
1123
|
+
const results = [];
|
|
1124
|
+
for (let i = 0; i < items.length; i++) {
|
|
1125
|
+
// Inject loop variables
|
|
1126
|
+
context.variables[step.itemVariable] = items[i];
|
|
1127
|
+
context.variables['loop'] = {
|
|
1128
|
+
index: i,
|
|
1129
|
+
first: i === 0,
|
|
1130
|
+
last: i === items.length - 1,
|
|
1131
|
+
length: items.length,
|
|
1132
|
+
};
|
|
1133
|
+
if (step.indexVariable) {
|
|
1134
|
+
context.variables[step.indexVariable] = i;
|
|
1135
|
+
}
|
|
1136
|
+
// Execute iteration steps
|
|
1137
|
+
for (const iterStep of step.steps) {
|
|
1138
|
+
const result = await this.executeStep(iterStep, context, sdkRegistry, stepExecutor);
|
|
1139
|
+
if (result.status === StepStatus.COMPLETED && iterStep.outputVariable) {
|
|
1140
|
+
context.variables[iterStep.outputVariable] = result.output;
|
|
1141
|
+
}
|
|
1142
|
+
if (result.status === StepStatus.FAILED) {
|
|
1143
|
+
const errorAction = step.errorHandling?.action ?? 'stop';
|
|
1144
|
+
if (errorAction === 'stop') {
|
|
1145
|
+
// Clean up loop variables
|
|
1146
|
+
delete context.variables[step.itemVariable];
|
|
1147
|
+
delete context.variables['loop'];
|
|
1148
|
+
if (step.indexVariable)
|
|
1149
|
+
delete context.variables[step.indexVariable];
|
|
1150
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
1151
|
+
}
|
|
1152
|
+
// 'continue' - skip to next iteration
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
results.push(context.variables[step.itemVariable]);
|
|
1157
|
+
}
|
|
1158
|
+
// Clean up loop variables
|
|
1159
|
+
delete context.variables[step.itemVariable];
|
|
1160
|
+
delete context.variables['loop'];
|
|
1161
|
+
if (step.indexVariable)
|
|
1162
|
+
delete context.variables[step.indexVariable];
|
|
1163
|
+
return createStepResult(step.id, StepStatus.COMPLETED, results, startedAt);
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
// Clean up loop variables on error
|
|
1167
|
+
delete context.variables[step.itemVariable];
|
|
1168
|
+
delete context.variables['loop'];
|
|
1169
|
+
if (step.indexVariable)
|
|
1170
|
+
delete context.variables[step.indexVariable];
|
|
1171
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Execute a while loop step.
|
|
1176
|
+
*/
|
|
1177
|
+
async executeWhileStep(step, context, sdkRegistry, stepExecutor) {
|
|
1178
|
+
const startedAt = new Date();
|
|
1179
|
+
let iterations = 0;
|
|
1180
|
+
try {
|
|
1181
|
+
while (this.evaluateCondition(step.condition, context)) {
|
|
1182
|
+
if (iterations >= step.maxIterations) {
|
|
1183
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, `Max iterations (${step.maxIterations}) exceeded`);
|
|
1184
|
+
}
|
|
1185
|
+
// Execute iteration steps
|
|
1186
|
+
for (const iterStep of step.steps) {
|
|
1187
|
+
const result = await this.executeStep(iterStep, context, sdkRegistry, stepExecutor);
|
|
1188
|
+
if (result.status === StepStatus.COMPLETED && iterStep.outputVariable) {
|
|
1189
|
+
context.variables[iterStep.outputVariable] = result.output;
|
|
1190
|
+
}
|
|
1191
|
+
if (result.status === StepStatus.FAILED) {
|
|
1192
|
+
const errorAction = step.errorHandling?.action ?? 'stop';
|
|
1193
|
+
if (errorAction === 'stop') {
|
|
1194
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
1195
|
+
}
|
|
1196
|
+
// 'continue' - skip to next iteration
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
iterations++;
|
|
1201
|
+
}
|
|
1202
|
+
return createStepResult(step.id, StepStatus.COMPLETED, { iterations }, startedAt);
|
|
1203
|
+
}
|
|
1204
|
+
catch (error) {
|
|
1205
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Execute a map transformation step.
|
|
1210
|
+
*/
|
|
1211
|
+
async executeMapStep(step, context, _sdkRegistry, _stepExecutor) {
|
|
1212
|
+
const startedAt = new Date();
|
|
1213
|
+
try {
|
|
1214
|
+
// Resolve items array
|
|
1215
|
+
const items = resolveTemplates(step.items, context);
|
|
1216
|
+
if (!Array.isArray(items)) {
|
|
1217
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
|
|
1218
|
+
}
|
|
1219
|
+
// Map each item using the expression
|
|
1220
|
+
const mapped = items.map((item) => {
|
|
1221
|
+
context.variables[step.itemVariable] = item;
|
|
1222
|
+
const result = resolveTemplates(step.expression, context);
|
|
1223
|
+
delete context.variables[step.itemVariable];
|
|
1224
|
+
return result;
|
|
1225
|
+
});
|
|
1226
|
+
return createStepResult(step.id, StepStatus.COMPLETED, mapped, startedAt);
|
|
1227
|
+
}
|
|
1228
|
+
catch (error) {
|
|
1229
|
+
delete context.variables[step.itemVariable];
|
|
1230
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Execute a filter step.
|
|
1235
|
+
*/
|
|
1236
|
+
async executeFilterStep(step, context, _sdkRegistry, _stepExecutor) {
|
|
1237
|
+
const startedAt = new Date();
|
|
1238
|
+
try {
|
|
1239
|
+
// Resolve items array
|
|
1240
|
+
const items = resolveTemplates(step.items, context);
|
|
1241
|
+
if (!Array.isArray(items)) {
|
|
1242
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
|
|
1243
|
+
}
|
|
1244
|
+
// Filter items using the condition
|
|
1245
|
+
const filtered = items.filter((item) => {
|
|
1246
|
+
context.variables[step.itemVariable] = item;
|
|
1247
|
+
const result = this.evaluateCondition(step.condition, context);
|
|
1248
|
+
delete context.variables[step.itemVariable];
|
|
1249
|
+
return result;
|
|
1250
|
+
});
|
|
1251
|
+
return createStepResult(step.id, StepStatus.COMPLETED, filtered, startedAt);
|
|
1252
|
+
}
|
|
1253
|
+
catch (error) {
|
|
1254
|
+
delete context.variables[step.itemVariable];
|
|
1255
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Execute a reduce/aggregate step.
|
|
1260
|
+
*/
|
|
1261
|
+
async executeReduceStep(step, context, _sdkRegistry, _stepExecutor) {
|
|
1262
|
+
const startedAt = new Date();
|
|
1263
|
+
try {
|
|
1264
|
+
// Resolve items array
|
|
1265
|
+
const items = resolveTemplates(step.items, context);
|
|
1266
|
+
if (!Array.isArray(items)) {
|
|
1267
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
|
|
1268
|
+
}
|
|
1269
|
+
// Reduce items using the expression
|
|
1270
|
+
let accumulator = step.initialValue ?? null;
|
|
1271
|
+
for (const item of items) {
|
|
1272
|
+
context.variables[step.itemVariable] = item;
|
|
1273
|
+
context.variables[step.accumulatorVariable] = accumulator;
|
|
1274
|
+
accumulator = resolveTemplates(step.expression, context);
|
|
1275
|
+
delete context.variables[step.itemVariable];
|
|
1276
|
+
delete context.variables[step.accumulatorVariable];
|
|
1277
|
+
}
|
|
1278
|
+
return createStepResult(step.id, StepStatus.COMPLETED, accumulator, startedAt);
|
|
1279
|
+
}
|
|
1280
|
+
catch (error) {
|
|
1281
|
+
delete context.variables[step.itemVariable];
|
|
1282
|
+
delete context.variables[step.accumulatorVariable];
|
|
1283
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Execute parallel branches.
|
|
1288
|
+
*/
|
|
1289
|
+
async executeParallelStep(step, context, sdkRegistry, stepExecutor) {
|
|
1290
|
+
const startedAt = new Date();
|
|
1291
|
+
try {
|
|
1292
|
+
// Execute branches in parallel
|
|
1293
|
+
const branchPromises = step.branches.map(async (branch) => {
|
|
1294
|
+
// Clone context for isolation
|
|
1295
|
+
const branchContext = this.cloneContext(context);
|
|
1296
|
+
// Execute branch steps
|
|
1297
|
+
const branchResults = [];
|
|
1298
|
+
for (const branchStep of branch.steps) {
|
|
1299
|
+
const result = await this.executeStep(branchStep, branchContext, sdkRegistry, stepExecutor);
|
|
1300
|
+
if (result.status === StepStatus.COMPLETED && branchStep.outputVariable) {
|
|
1301
|
+
branchContext.variables[branchStep.outputVariable] = result.output;
|
|
1302
|
+
branchResults.push(result.output);
|
|
1303
|
+
}
|
|
1304
|
+
if (result.status === StepStatus.FAILED) {
|
|
1305
|
+
throw new Error(`Branch ${branch.id} failed: ${errorToString(result.error)}`);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return { branchId: branch.id, context: branchContext, results: branchResults };
|
|
1309
|
+
});
|
|
1310
|
+
// Wait for all branches (or limited concurrency)
|
|
1311
|
+
const branchResults = step.maxConcurrent
|
|
1312
|
+
? await this.executeConcurrentlyWithLimit(branchPromises, step.maxConcurrent)
|
|
1313
|
+
: await Promise.all(branchPromises);
|
|
1314
|
+
// Merge branch contexts back into main context
|
|
1315
|
+
for (const { branchId, context: branchContext } of branchResults) {
|
|
1316
|
+
this.mergeContexts(context, branchContext, branchId);
|
|
1317
|
+
}
|
|
1318
|
+
const outputs = branchResults.map((br) => br.results);
|
|
1319
|
+
return createStepResult(step.id, StepStatus.COMPLETED, outputs, startedAt);
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
if (step.onError === 'continue') {
|
|
1323
|
+
return createStepResult(step.id, StepStatus.COMPLETED, null, startedAt);
|
|
1324
|
+
}
|
|
1325
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Execute try/catch/finally step.
|
|
1330
|
+
*/
|
|
1331
|
+
async executeTryStep(step, context, sdkRegistry, stepExecutor) {
|
|
1332
|
+
const startedAt = new Date();
|
|
1333
|
+
let tryError;
|
|
1334
|
+
try {
|
|
1335
|
+
// Execute try block
|
|
1336
|
+
for (const tryStep of step.try) {
|
|
1337
|
+
const result = await this.executeStep(tryStep, context, sdkRegistry, stepExecutor);
|
|
1338
|
+
if (result.status === StepStatus.COMPLETED && tryStep.outputVariable) {
|
|
1339
|
+
context.variables[tryStep.outputVariable] = result.output;
|
|
1340
|
+
}
|
|
1341
|
+
if (result.status === StepStatus.FAILED) {
|
|
1342
|
+
tryError = new Error(result.error ? errorToString(result.error) : 'Step failed');
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
// If error occurred and catch block exists, execute catch
|
|
1347
|
+
let catchError;
|
|
1348
|
+
if (tryError && step.catch) {
|
|
1349
|
+
// Inject error object into context
|
|
1350
|
+
context.variables['error'] = {
|
|
1351
|
+
message: tryError.message,
|
|
1352
|
+
step: tryError,
|
|
1353
|
+
};
|
|
1354
|
+
for (const catchStep of step.catch) {
|
|
1355
|
+
const result = await this.executeStep(catchStep, context, sdkRegistry, stepExecutor);
|
|
1356
|
+
if (result.status === StepStatus.COMPLETED && catchStep.outputVariable) {
|
|
1357
|
+
context.variables[catchStep.outputVariable] = result.output;
|
|
1358
|
+
}
|
|
1359
|
+
if (result.status === StepStatus.FAILED) {
|
|
1360
|
+
catchError = new Error(result.error ? errorToString(result.error) : 'Catch block failed');
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
delete context.variables['error'];
|
|
1365
|
+
}
|
|
1366
|
+
// Execute finally block (always runs)
|
|
1367
|
+
if (step.finally) {
|
|
1368
|
+
for (const finallyStep of step.finally) {
|
|
1369
|
+
const result = await this.executeStep(finallyStep, context, sdkRegistry, stepExecutor);
|
|
1370
|
+
if (result.status === StepStatus.COMPLETED && finallyStep.outputVariable) {
|
|
1371
|
+
context.variables[finallyStep.outputVariable] = result.output;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
// Return success if catch handled the error, or error if not
|
|
1376
|
+
if (tryError && !step.catch) {
|
|
1377
|
+
// No catch block to handle error
|
|
1378
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, tryError.message);
|
|
1379
|
+
}
|
|
1380
|
+
if (catchError) {
|
|
1381
|
+
// Catch block also failed
|
|
1382
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, catchError.message);
|
|
1383
|
+
}
|
|
1384
|
+
return createStepResult(step.id, StepStatus.COMPLETED, null, startedAt);
|
|
1385
|
+
}
|
|
1386
|
+
catch (error) {
|
|
1387
|
+
// Execute finally even on unexpected error
|
|
1388
|
+
if (step.finally) {
|
|
1389
|
+
try {
|
|
1390
|
+
for (const finallyStep of step.finally) {
|
|
1391
|
+
const result = await this.executeStep(finallyStep, context, sdkRegistry, stepExecutor);
|
|
1392
|
+
if (result.status === StepStatus.COMPLETED && finallyStep.outputVariable) {
|
|
1393
|
+
context.variables[finallyStep.outputVariable] = result.output;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
catch {
|
|
1398
|
+
// Ignore finally errors
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// ============================================================================
|
|
1405
|
+
// Helper Methods for Control Flow
|
|
1406
|
+
// ============================================================================
|
|
1407
|
+
/**
|
|
1408
|
+
* Clone execution context for parallel branches.
|
|
1409
|
+
*/
|
|
1410
|
+
cloneContext(context) {
|
|
1411
|
+
return {
|
|
1412
|
+
...context,
|
|
1413
|
+
variables: { ...context.variables },
|
|
1414
|
+
inputs: { ...context.inputs },
|
|
1415
|
+
stepMetadata: { ...context.stepMetadata },
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Merge branch context back into main context.
|
|
1420
|
+
*/
|
|
1421
|
+
mergeContexts(mainContext, branchContext, branchId) {
|
|
1422
|
+
// Merge variables with branch prefix
|
|
1423
|
+
for (const [key, value] of Object.entries(branchContext.variables)) {
|
|
1424
|
+
mainContext.variables[`${branchId}.${key}`] = value;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Execute promises with concurrency limit.
|
|
1429
|
+
*/
|
|
1430
|
+
async executeConcurrentlyWithLimit(promises, limit) {
|
|
1431
|
+
const results = [];
|
|
1432
|
+
const executing = [];
|
|
1433
|
+
for (const promise of promises) {
|
|
1434
|
+
const p = promise.then((result) => {
|
|
1435
|
+
results.push(result);
|
|
1436
|
+
});
|
|
1437
|
+
executing.push(p);
|
|
1438
|
+
if (executing.length >= limit) {
|
|
1439
|
+
await Promise.race(executing);
|
|
1440
|
+
executing.splice(executing.findIndex((x) => x === p), 1);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
await Promise.all(executing);
|
|
1444
|
+
return results;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
// ============================================================================
|
|
1448
|
+
// Helpers
|
|
1449
|
+
// ============================================================================
|
|
1450
|
+
function sleep(ms) {
|
|
1451
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1452
|
+
}
|
|
1453
|
+
//# sourceMappingURL=engine.js.map
|