@marktoflow/core 2.0.0-alpha.7 → 2.0.1
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 +24 -220
- package/dist/built-in-operations.d.ts +150 -0
- package/dist/built-in-operations.d.ts.map +1 -0
- package/dist/built-in-operations.js +799 -0
- package/dist/built-in-operations.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/credentials.d.ts +60 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +229 -4
- package/dist/credentials.js.map +1 -1
- package/dist/engine.d.ts +144 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1385 -49
- package/dist/engine.js.map +1 -1
- package/dist/file-operations.d.ts +86 -0
- package/dist/file-operations.d.ts.map +1 -0
- package/dist/file-operations.js +363 -0
- package/dist/file-operations.js.map +1 -0
- package/dist/index.d.ts +16 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -4
- package/dist/index.js.map +1 -1
- package/dist/logging.d.ts +40 -2
- package/dist/logging.d.ts.map +1 -1
- package/dist/logging.js +166 -13
- package/dist/logging.js.map +1 -1
- package/dist/models.d.ts +1931 -203
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +303 -13
- package/dist/models.js.map +1 -1
- package/dist/nunjucks-filters.d.ts +271 -0
- package/dist/nunjucks-filters.d.ts.map +1 -0
- package/dist/nunjucks-filters.js +648 -0
- package/dist/nunjucks-filters.js.map +1 -0
- package/dist/oauth-manager.d.ts +128 -0
- package/dist/oauth-manager.d.ts.map +1 -0
- package/dist/oauth-manager.js +291 -0
- package/dist/oauth-manager.js.map +1 -0
- package/dist/oauth-refresh.d.ts +37 -0
- package/dist/oauth-refresh.d.ts.map +1 -0
- package/dist/oauth-refresh.js +76 -0
- package/dist/oauth-refresh.js.map +1 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +291 -10
- package/dist/parser.js.map +1 -1
- 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/prompt-loader.d.ts +53 -0
- package/dist/prompt-loader.d.ts.map +1 -0
- package/dist/prompt-loader.js +205 -0
- package/dist/prompt-loader.js.map +1 -0
- package/dist/scheduler.d.ts +22 -3
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +72 -73
- package/dist/scheduler.js.map +1 -1
- package/dist/script-executor.d.ts +65 -0
- package/dist/script-executor.d.ts.map +1 -0
- package/dist/script-executor.js +261 -0
- package/dist/script-executor.js.map +1 -0
- package/dist/sdk-registry.d.ts +20 -2
- package/dist/sdk-registry.d.ts.map +1 -1
- package/dist/sdk-registry.js +100 -15
- package/dist/sdk-registry.js.map +1 -1
- package/dist/secret-providers/index.d.ts +12 -0
- package/dist/secret-providers/index.d.ts.map +1 -0
- package/dist/secret-providers/index.js +11 -0
- package/dist/secret-providers/index.js.map +1 -0
- package/dist/secret-providers/providers/aws.d.ts +32 -0
- package/dist/secret-providers/providers/aws.d.ts.map +1 -0
- package/dist/secret-providers/providers/aws.js +118 -0
- package/dist/secret-providers/providers/aws.js.map +1 -0
- package/dist/secret-providers/providers/azure.d.ts +40 -0
- package/dist/secret-providers/providers/azure.d.ts.map +1 -0
- package/dist/secret-providers/providers/azure.js +170 -0
- package/dist/secret-providers/providers/azure.js.map +1 -0
- package/dist/secret-providers/providers/env.d.ts +26 -0
- package/dist/secret-providers/providers/env.d.ts.map +1 -0
- package/dist/secret-providers/providers/env.js +59 -0
- package/dist/secret-providers/providers/env.js.map +1 -0
- package/dist/secret-providers/providers/vault.d.ts +39 -0
- package/dist/secret-providers/providers/vault.d.ts.map +1 -0
- package/dist/secret-providers/providers/vault.js +180 -0
- package/dist/secret-providers/providers/vault.js.map +1 -0
- package/dist/secret-providers/secret-manager.d.ts +72 -0
- package/dist/secret-providers/secret-manager.d.ts.map +1 -0
- package/dist/secret-providers/secret-manager.js +226 -0
- package/dist/secret-providers/secret-manager.js.map +1 -0
- package/dist/secret-providers/types.d.ts +105 -0
- package/dist/secret-providers/types.d.ts.map +1 -0
- package/dist/secret-providers/types.js +8 -0
- package/dist/secret-providers/types.js.map +1 -0
- package/dist/security.d.ts +1 -0
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +4 -0
- package/dist/security.js.map +1 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +16 -9
- package/dist/state.js.map +1 -1
- package/dist/template-engine.d.ts +51 -0
- package/dist/template-engine.d.ts.map +1 -0
- package/dist/template-engine.js +227 -0
- package/dist/template-engine.js.map +1 -0
- package/dist/templates.d.ts +10 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +21 -17
- package/dist/templates.js.map +1 -1
- package/dist/tools/mcp-tool.js +9 -9
- package/dist/tools/mcp-tool.js.map +1 -1
- package/dist/trigger-manager.js +1 -1
- package/dist/trigger-manager.js.map +1 -1
- 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 +24 -6
package/dist/engine.js
CHANGED
|
@@ -4,10 +4,72 @@
|
|
|
4
4
|
* Executes workflow steps with retry logic, variable resolution,
|
|
5
5
|
* and SDK invocation.
|
|
6
6
|
*/
|
|
7
|
-
import { StepStatus, WorkflowStatus, createExecutionContext, createStepResult, } from './models.js';
|
|
7
|
+
import { StepStatus, WorkflowStatus, createExecutionContext, createStepResult, isActionStep, isSubWorkflowStep, isIfStep, isSwitchStep, isForEachStep, isWhileStep, isMapStep, isFilterStep, isReduceStep, isParallelStep, isTryStep, isScriptStep, isWaitStep, isMergeStep, } from './models.js';
|
|
8
|
+
import { mergePermissions, toSecurityPolicy, } from './permissions.js';
|
|
9
|
+
import { loadPromptFile, resolvePromptTemplate, validatePromptInputs, } from './prompt-loader.js';
|
|
8
10
|
import { DEFAULT_FAILOVER_CONFIG, AgentHealthTracker, FailoverReason, } from './failover.js';
|
|
9
11
|
import { parseFile } from './parser.js';
|
|
10
12
|
import { resolve, dirname } from 'node:path';
|
|
13
|
+
import { executeBuiltInOperation, isBuiltInOperation } from './built-in-operations.js';
|
|
14
|
+
import { renderTemplate } from './template-engine.js';
|
|
15
|
+
import { executeScriptAsync } from './script-executor.js';
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Helper Functions
|
|
18
|
+
// ============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Parse a duration string like "2h", "30m", "5s", "100ms" into milliseconds.
|
|
21
|
+
*/
|
|
22
|
+
function parseDuration(duration) {
|
|
23
|
+
const match = duration.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i);
|
|
24
|
+
if (!match) {
|
|
25
|
+
const asNum = Number(duration);
|
|
26
|
+
if (!isNaN(asNum))
|
|
27
|
+
return asNum; // Treat bare numbers as milliseconds
|
|
28
|
+
throw new Error(`Invalid duration format: "${duration}". Use format like "2h", "30m", "5s", "100ms"`);
|
|
29
|
+
}
|
|
30
|
+
const value = parseFloat(match[1]);
|
|
31
|
+
const unit = match[2].toLowerCase();
|
|
32
|
+
switch (unit) {
|
|
33
|
+
case 'ms': return value;
|
|
34
|
+
case 's': return value * 1000;
|
|
35
|
+
case 'm': return value * 60 * 1000;
|
|
36
|
+
case 'h': return value * 60 * 60 * 1000;
|
|
37
|
+
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
38
|
+
default: return value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Convert error to string for display/logging
|
|
43
|
+
*/
|
|
44
|
+
function errorToString(error) {
|
|
45
|
+
if (!error)
|
|
46
|
+
return 'Unknown error';
|
|
47
|
+
if (typeof error === 'string')
|
|
48
|
+
return error;
|
|
49
|
+
if (error instanceof Error)
|
|
50
|
+
return error.message;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.stringify(error);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return String(error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get a field value from an object by key path.
|
|
60
|
+
*/
|
|
61
|
+
function getField(item, field) {
|
|
62
|
+
if (!item || typeof item !== 'object')
|
|
63
|
+
return undefined;
|
|
64
|
+
const parts = field.split('.');
|
|
65
|
+
let current = item;
|
|
66
|
+
for (const part of parts) {
|
|
67
|
+
if (!current || typeof current !== 'object')
|
|
68
|
+
return undefined;
|
|
69
|
+
current = current[part];
|
|
70
|
+
}
|
|
71
|
+
return current;
|
|
72
|
+
}
|
|
11
73
|
// ============================================================================
|
|
12
74
|
// Retry Policy
|
|
13
75
|
// ============================================================================
|
|
@@ -92,15 +154,25 @@ export class CircuitBreaker {
|
|
|
92
154
|
// ============================================================================
|
|
93
155
|
/**
|
|
94
156
|
* Resolve template variables in a value.
|
|
95
|
-
* Supports {{variable}}
|
|
157
|
+
* Supports {{variable}}, {{inputs.name}}, and Nunjucks filters.
|
|
158
|
+
*
|
|
159
|
+
* Uses Nunjucks as the template engine with:
|
|
160
|
+
* - Legacy regex operator support (=~, !~, //) converted to filters
|
|
161
|
+
* - Custom filters for string, array, object, date operations
|
|
162
|
+
* - Jinja2-style control flow ({% for %}, {% if %}, etc.)
|
|
96
163
|
*/
|
|
97
164
|
export function resolveTemplates(value, context) {
|
|
98
165
|
if (typeof value === 'string') {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
166
|
+
// Build the template context with all available variables
|
|
167
|
+
// Spread inputs first, then variables (variables override inputs if same key)
|
|
168
|
+
// Also keep inputs accessible via inputs.* for explicit access
|
|
169
|
+
const templateContext = {
|
|
170
|
+
...context.inputs, // Spread inputs at root level for direct access ({{ path }})
|
|
171
|
+
...context.variables, // Variables override inputs if same key
|
|
172
|
+
inputs: context.inputs, // Also keep inputs accessible as inputs.*
|
|
173
|
+
};
|
|
174
|
+
// Use the new Nunjucks-based template engine with legacy syntax support
|
|
175
|
+
return renderTemplate(value, templateContext);
|
|
104
176
|
}
|
|
105
177
|
if (Array.isArray(value)) {
|
|
106
178
|
return value.map((v) => resolveTemplates(v, context));
|
|
@@ -130,6 +202,11 @@ export function resolveVariablePath(path, context) {
|
|
|
130
202
|
if (fromVars !== undefined) {
|
|
131
203
|
return fromVars;
|
|
132
204
|
}
|
|
205
|
+
// Check inputs (for bare variable names like "value" instead of "inputs.value")
|
|
206
|
+
const fromInputs = getNestedValue(context.inputs, path);
|
|
207
|
+
if (fromInputs !== undefined) {
|
|
208
|
+
return fromInputs;
|
|
209
|
+
}
|
|
133
210
|
// Check step metadata (for status checks like: step_id.status)
|
|
134
211
|
const fromStepMeta = getNestedValue(context.stepMetadata, path);
|
|
135
212
|
if (fromStepMeta !== undefined) {
|
|
@@ -139,26 +216,63 @@ export function resolveVariablePath(path, context) {
|
|
|
139
216
|
return getNestedValue(context, path);
|
|
140
217
|
}
|
|
141
218
|
/**
|
|
142
|
-
* Get a nested value from an object using dot notation.
|
|
219
|
+
* Get a nested value from an object using dot notation and array indexing.
|
|
220
|
+
* Supports paths like: "user.name", "items[0].name", "data.users[1].email"
|
|
143
221
|
*/
|
|
144
222
|
function getNestedValue(obj, path) {
|
|
145
223
|
if (obj === null || obj === undefined) {
|
|
146
224
|
return undefined;
|
|
147
225
|
}
|
|
148
|
-
|
|
149
|
-
|
|
226
|
+
// Parse path into parts, handling both dot notation and array indexing
|
|
227
|
+
// Convert "a.b[0].c[1]" into ["a", "b", "0", "c", "1"]
|
|
228
|
+
const parts = [];
|
|
229
|
+
let current = '';
|
|
230
|
+
for (let i = 0; i < path.length; i++) {
|
|
231
|
+
const char = path[i];
|
|
232
|
+
if (char === '.') {
|
|
233
|
+
if (current) {
|
|
234
|
+
parts.push(current);
|
|
235
|
+
current = '';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (char === '[') {
|
|
239
|
+
if (current) {
|
|
240
|
+
parts.push(current);
|
|
241
|
+
current = '';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else if (char === ']') {
|
|
245
|
+
if (current) {
|
|
246
|
+
parts.push(current);
|
|
247
|
+
current = '';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
current += char;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (current) {
|
|
255
|
+
parts.push(current);
|
|
256
|
+
}
|
|
257
|
+
// Traverse the object using the parsed parts
|
|
258
|
+
let result = obj;
|
|
150
259
|
for (const part of parts) {
|
|
151
|
-
if (
|
|
260
|
+
if (result === null || result === undefined) {
|
|
152
261
|
return undefined;
|
|
153
262
|
}
|
|
154
|
-
if
|
|
155
|
-
|
|
263
|
+
// Check if part is a number (array index)
|
|
264
|
+
const index = Number(part);
|
|
265
|
+
if (!isNaN(index) && Array.isArray(result)) {
|
|
266
|
+
result = result[index];
|
|
267
|
+
}
|
|
268
|
+
else if (typeof result === 'object') {
|
|
269
|
+
result = result[part];
|
|
156
270
|
}
|
|
157
271
|
else {
|
|
158
272
|
return undefined;
|
|
159
273
|
}
|
|
160
274
|
}
|
|
161
|
-
return
|
|
275
|
+
return result;
|
|
162
276
|
}
|
|
163
277
|
export class WorkflowEngine {
|
|
164
278
|
config;
|
|
@@ -170,13 +284,17 @@ export class WorkflowEngine {
|
|
|
170
284
|
failoverConfig;
|
|
171
285
|
healthTracker;
|
|
172
286
|
failoverEvents = [];
|
|
173
|
-
workflowPath; // Base path for resolving sub-workflows
|
|
287
|
+
workflowPath; // Base path for resolving sub-workflows (public for CLI state tracking)
|
|
288
|
+
workflowPermissions; // Workflow-level permissions
|
|
289
|
+
promptCache = new Map(); // Cache for loaded prompts
|
|
174
290
|
constructor(config = {}, events = {}, stateStore) {
|
|
175
291
|
this.config = {
|
|
176
292
|
defaultTimeout: config.defaultTimeout ?? 60000,
|
|
177
293
|
maxRetries: config.maxRetries ?? 3,
|
|
178
294
|
retryBaseDelay: config.retryBaseDelay ?? 1000,
|
|
179
295
|
retryMaxDelay: config.retryMaxDelay ?? 30000,
|
|
296
|
+
defaultAgent: config.defaultAgent,
|
|
297
|
+
defaultModel: config.defaultModel,
|
|
180
298
|
};
|
|
181
299
|
this.retryPolicy = new RetryPolicy(this.config.maxRetries, this.config.retryBaseDelay, this.config.retryMaxDelay);
|
|
182
300
|
this.events = events;
|
|
@@ -185,6 +303,54 @@ export class WorkflowEngine {
|
|
|
185
303
|
this.failoverConfig = { ...DEFAULT_FAILOVER_CONFIG, ...(config.failoverConfig ?? {}) };
|
|
186
304
|
this.healthTracker = config.healthTracker ?? new AgentHealthTracker();
|
|
187
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Execute a single step - dispatcher to specialized execution methods.
|
|
308
|
+
*/
|
|
309
|
+
async executeStep(step, context, sdkRegistry, stepExecutor) {
|
|
310
|
+
// Check conditions first (applies to all step types)
|
|
311
|
+
if (step.conditions && !this.evaluateConditions(step.conditions, context)) {
|
|
312
|
+
return createStepResult(step.id, StepStatus.SKIPPED, null, new Date());
|
|
313
|
+
}
|
|
314
|
+
// Dispatch to specialized execution method based on step type
|
|
315
|
+
if (isIfStep(step)) {
|
|
316
|
+
return this.executeIfStep(step, context, sdkRegistry, stepExecutor);
|
|
317
|
+
}
|
|
318
|
+
if (isSwitchStep(step)) {
|
|
319
|
+
return this.executeSwitchStep(step, context, sdkRegistry, stepExecutor);
|
|
320
|
+
}
|
|
321
|
+
if (isForEachStep(step)) {
|
|
322
|
+
return this.executeForEachStep(step, context, sdkRegistry, stepExecutor);
|
|
323
|
+
}
|
|
324
|
+
if (isWhileStep(step)) {
|
|
325
|
+
return this.executeWhileStep(step, context, sdkRegistry, stepExecutor);
|
|
326
|
+
}
|
|
327
|
+
if (isMapStep(step)) {
|
|
328
|
+
return this.executeMapStep(step, context, sdkRegistry, stepExecutor);
|
|
329
|
+
}
|
|
330
|
+
if (isFilterStep(step)) {
|
|
331
|
+
return this.executeFilterStep(step, context, sdkRegistry, stepExecutor);
|
|
332
|
+
}
|
|
333
|
+
if (isReduceStep(step)) {
|
|
334
|
+
return this.executeReduceStep(step, context, sdkRegistry, stepExecutor);
|
|
335
|
+
}
|
|
336
|
+
if (isParallelStep(step)) {
|
|
337
|
+
return this.executeParallelStep(step, context, sdkRegistry, stepExecutor);
|
|
338
|
+
}
|
|
339
|
+
if (isTryStep(step)) {
|
|
340
|
+
return this.executeTryStep(step, context, sdkRegistry, stepExecutor);
|
|
341
|
+
}
|
|
342
|
+
if (isScriptStep(step)) {
|
|
343
|
+
return this.executeScriptStep(step, context);
|
|
344
|
+
}
|
|
345
|
+
if (isWaitStep(step)) {
|
|
346
|
+
return this.executeWaitStep(step, context);
|
|
347
|
+
}
|
|
348
|
+
if (isMergeStep(step)) {
|
|
349
|
+
return this.executeMergeStep(step, context);
|
|
350
|
+
}
|
|
351
|
+
// Default: action or workflow step
|
|
352
|
+
return this.executeStepWithFailover(step, context, sdkRegistry, stepExecutor);
|
|
353
|
+
}
|
|
188
354
|
/**
|
|
189
355
|
* Execute a workflow.
|
|
190
356
|
*/
|
|
@@ -192,13 +358,22 @@ export class WorkflowEngine {
|
|
|
192
358
|
const context = createExecutionContext(workflow, inputs);
|
|
193
359
|
const stepResults = [];
|
|
194
360
|
const startedAt = new Date();
|
|
361
|
+
// Store workflow-level permissions and defaults
|
|
362
|
+
this.workflowPermissions = workflow.permissions;
|
|
363
|
+
// Use workflow defaults if not set in engine config
|
|
364
|
+
if (!this.config.defaultAgent && workflow.defaultAgent) {
|
|
365
|
+
this.config.defaultAgent = workflow.defaultAgent;
|
|
366
|
+
}
|
|
367
|
+
if (!this.config.defaultModel && workflow.defaultModel) {
|
|
368
|
+
this.config.defaultModel = workflow.defaultModel;
|
|
369
|
+
}
|
|
195
370
|
context.status = WorkflowStatus.RUNNING;
|
|
196
371
|
this.events.onWorkflowStart?.(workflow, context);
|
|
197
372
|
if (this.stateStore) {
|
|
198
373
|
this.stateStore.createExecution({
|
|
199
374
|
runId: context.runId,
|
|
200
375
|
workflowId: workflow.metadata.id,
|
|
201
|
-
workflowPath: 'unknown',
|
|
376
|
+
workflowPath: this.workflowPath ?? 'unknown',
|
|
202
377
|
status: WorkflowStatus.RUNNING,
|
|
203
378
|
startedAt: startedAt,
|
|
204
379
|
completedAt: null,
|
|
@@ -214,32 +389,39 @@ export class WorkflowEngine {
|
|
|
214
389
|
for (let i = 0; i < workflow.steps.length; i++) {
|
|
215
390
|
const step = workflow.steps[i];
|
|
216
391
|
context.currentStepIndex = i;
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
const skipResult = createStepResult(step.id, StepStatus.SKIPPED, null, new Date());
|
|
220
|
-
stepResults.push(skipResult);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
// Execute step with retry
|
|
224
|
-
const result = await this.executeStepWithFailover(step, context, sdkRegistry, stepExecutor);
|
|
392
|
+
// Execute step using dispatcher
|
|
393
|
+
const result = await this.executeStep(step, context, sdkRegistry, stepExecutor);
|
|
225
394
|
stepResults.push(result);
|
|
226
395
|
// Store step metadata (status, error, etc.) in separate field for condition evaluation
|
|
227
396
|
// This allows conditions like: step_id.status == 'failed'
|
|
228
397
|
context.stepMetadata[step.id] = {
|
|
229
398
|
status: result.status.toLowerCase(),
|
|
230
399
|
retryCount: result.retryCount,
|
|
231
|
-
...(result.error ? { error: result.error } : {}),
|
|
400
|
+
...(result.error ? { error: errorToString(result.error) } : {}),
|
|
232
401
|
};
|
|
233
402
|
// Store output variable
|
|
234
403
|
if (step.outputVariable && result.status === StepStatus.COMPLETED) {
|
|
235
404
|
context.variables[step.outputVariable] = result.output;
|
|
236
405
|
}
|
|
406
|
+
// Check if this step set workflow outputs (from workflow.set_outputs action)
|
|
407
|
+
if (result.status === StepStatus.COMPLETED &&
|
|
408
|
+
result.output &&
|
|
409
|
+
typeof result.output === 'object' &&
|
|
410
|
+
'__workflow_outputs__' in result.output) {
|
|
411
|
+
const outputObj = result.output;
|
|
412
|
+
const outputs = outputObj['__workflow_outputs__'];
|
|
413
|
+
context.workflowOutputs = outputs;
|
|
414
|
+
}
|
|
237
415
|
// Handle failure
|
|
238
416
|
if (result.status === StepStatus.FAILED) {
|
|
239
|
-
|
|
417
|
+
// Get error action from step if it has error handling
|
|
418
|
+
let errorAction = 'stop';
|
|
419
|
+
if ('errorHandling' in step && step.errorHandling?.action) {
|
|
420
|
+
errorAction = step.errorHandling.action;
|
|
421
|
+
}
|
|
240
422
|
if (errorAction === 'stop') {
|
|
241
423
|
context.status = WorkflowStatus.FAILED;
|
|
242
|
-
const workflowError = result.error
|
|
424
|
+
const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
|
|
243
425
|
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
|
|
244
426
|
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
245
427
|
return workflowResult;
|
|
@@ -254,7 +436,7 @@ export class WorkflowEngine {
|
|
|
254
436
|
});
|
|
255
437
|
}
|
|
256
438
|
context.status = WorkflowStatus.FAILED;
|
|
257
|
-
const workflowError = result.error
|
|
439
|
+
const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
|
|
258
440
|
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
|
|
259
441
|
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
260
442
|
return workflowResult;
|
|
@@ -288,6 +470,136 @@ export class WorkflowEngine {
|
|
|
288
470
|
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
289
471
|
return workflowResult;
|
|
290
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Resume a paused execution (e.g., after form submission).
|
|
475
|
+
*
|
|
476
|
+
* @param runId - The execution run ID
|
|
477
|
+
* @param stepId - The step ID that was waiting
|
|
478
|
+
* @param resumeData - Data from the resume action (e.g., form submission)
|
|
479
|
+
* @param sdkRegistry - SDK registry for step execution
|
|
480
|
+
* @param stepExecutor - Step executor function
|
|
481
|
+
* @returns Workflow result from resumed execution
|
|
482
|
+
*/
|
|
483
|
+
async resumeExecution(runId, stepId, resumeData, sdkRegistry, stepExecutor) {
|
|
484
|
+
if (!this.stateStore) {
|
|
485
|
+
throw new Error('Cannot resume execution: StateStore not configured');
|
|
486
|
+
}
|
|
487
|
+
// Load execution from state store
|
|
488
|
+
const execution = this.stateStore.getExecution(runId);
|
|
489
|
+
if (!execution) {
|
|
490
|
+
throw new Error(`Execution ${runId} not found`);
|
|
491
|
+
}
|
|
492
|
+
if (execution.status !== WorkflowStatus.RUNNING) {
|
|
493
|
+
throw new Error(`Cannot resume execution ${runId}: status is ${execution.status}`);
|
|
494
|
+
}
|
|
495
|
+
// Load workflow
|
|
496
|
+
const { workflow } = await parseFile(execution.workflowPath);
|
|
497
|
+
this.workflowPath = execution.workflowPath;
|
|
498
|
+
// Find the step that was waiting
|
|
499
|
+
const stepIndex = workflow.steps.findIndex(s => s.id === stepId);
|
|
500
|
+
if (stepIndex === -1) {
|
|
501
|
+
throw new Error(`Step ${stepId} not found in workflow`);
|
|
502
|
+
}
|
|
503
|
+
// Load all checkpoints to rebuild context
|
|
504
|
+
const checkpoints = this.stateStore.getCheckpoints(runId);
|
|
505
|
+
const stepResults = [];
|
|
506
|
+
const context = createExecutionContext(workflow, execution.inputs || {});
|
|
507
|
+
context.runId = runId;
|
|
508
|
+
context.status = WorkflowStatus.RUNNING;
|
|
509
|
+
// Rebuild context from checkpoints
|
|
510
|
+
for (let i = 0; i < stepIndex; i++) {
|
|
511
|
+
const checkpoint = checkpoints.find(cp => cp.stepIndex === i);
|
|
512
|
+
if (checkpoint) {
|
|
513
|
+
const step = workflow.steps[i];
|
|
514
|
+
// Recreate step result
|
|
515
|
+
const result = createStepResult(step.id, checkpoint.status, checkpoint.outputs, checkpoint.startedAt, checkpoint.retryCount, checkpoint.error || undefined);
|
|
516
|
+
stepResults.push(result);
|
|
517
|
+
// Restore step metadata
|
|
518
|
+
context.stepMetadata[step.id] = {
|
|
519
|
+
status: checkpoint.status.toLowerCase(),
|
|
520
|
+
retryCount: checkpoint.retryCount,
|
|
521
|
+
...(checkpoint.error ? { error: checkpoint.error } : {}),
|
|
522
|
+
};
|
|
523
|
+
// Restore output variable
|
|
524
|
+
if (step.outputVariable && checkpoint.status === StepStatus.COMPLETED) {
|
|
525
|
+
context.variables[step.outputVariable] = checkpoint.outputs;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Inject resume data into context
|
|
530
|
+
context.variables[`${stepId}_response`] = resumeData;
|
|
531
|
+
// Continue execution from next step
|
|
532
|
+
const startedAt = new Date(execution.startedAt);
|
|
533
|
+
this.workflowPermissions = workflow.permissions;
|
|
534
|
+
try {
|
|
535
|
+
for (let i = stepIndex + 1; i < workflow.steps.length; i++) {
|
|
536
|
+
const step = workflow.steps[i];
|
|
537
|
+
context.currentStepIndex = i;
|
|
538
|
+
const result = await this.executeStep(step, context, sdkRegistry, stepExecutor);
|
|
539
|
+
stepResults.push(result);
|
|
540
|
+
context.stepMetadata[step.id] = {
|
|
541
|
+
status: result.status.toLowerCase(),
|
|
542
|
+
retryCount: result.retryCount,
|
|
543
|
+
...(result.error ? { error: errorToString(result.error) } : {}),
|
|
544
|
+
};
|
|
545
|
+
if (step.outputVariable && result.status === StepStatus.COMPLETED) {
|
|
546
|
+
context.variables[step.outputVariable] = result.output;
|
|
547
|
+
}
|
|
548
|
+
if (result.status === StepStatus.COMPLETED &&
|
|
549
|
+
result.output &&
|
|
550
|
+
typeof result.output === 'object' &&
|
|
551
|
+
'__workflow_outputs__' in result.output) {
|
|
552
|
+
const outputObj = result.output;
|
|
553
|
+
const outputs = outputObj['__workflow_outputs__'];
|
|
554
|
+
context.workflowOutputs = outputs;
|
|
555
|
+
}
|
|
556
|
+
if (result.status === StepStatus.FAILED) {
|
|
557
|
+
let errorAction = 'stop';
|
|
558
|
+
if ('errorHandling' in step && step.errorHandling?.action) {
|
|
559
|
+
errorAction = step.errorHandling.action;
|
|
560
|
+
}
|
|
561
|
+
if (errorAction === 'stop' || errorAction === 'rollback') {
|
|
562
|
+
if (errorAction === 'rollback' && this.rollbackRegistry) {
|
|
563
|
+
await this.rollbackRegistry.rollbackAllAsync({
|
|
564
|
+
context,
|
|
565
|
+
inputs: context.inputs,
|
|
566
|
+
variables: context.variables,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
context.status = WorkflowStatus.FAILED;
|
|
570
|
+
const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
|
|
571
|
+
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
|
|
572
|
+
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
573
|
+
return workflowResult;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
context.status = WorkflowStatus.COMPLETED;
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
context.status = WorkflowStatus.FAILED;
|
|
581
|
+
if (this.stateStore) {
|
|
582
|
+
this.stateStore.updateExecution(runId, {
|
|
583
|
+
status: WorkflowStatus.FAILED,
|
|
584
|
+
completedAt: new Date(),
|
|
585
|
+
error: error instanceof Error ? error.message : String(error),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, error instanceof Error ? error.message : String(error));
|
|
589
|
+
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
590
|
+
return workflowResult;
|
|
591
|
+
}
|
|
592
|
+
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt);
|
|
593
|
+
if (this.stateStore) {
|
|
594
|
+
this.stateStore.updateExecution(runId, {
|
|
595
|
+
status: context.status,
|
|
596
|
+
completedAt: new Date(),
|
|
597
|
+
outputs: context.variables,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
601
|
+
return workflowResult;
|
|
602
|
+
}
|
|
291
603
|
/**
|
|
292
604
|
* Execute a workflow from a file.
|
|
293
605
|
* This method automatically sets the workflow path for resolving sub-workflows.
|
|
@@ -307,8 +619,8 @@ export class WorkflowEngine {
|
|
|
307
619
|
* Execute a sub-workflow.
|
|
308
620
|
*/
|
|
309
621
|
async executeSubWorkflow(step, context, sdkRegistry, stepExecutor) {
|
|
310
|
-
if (!step
|
|
311
|
-
throw new Error(`Step ${step.id}
|
|
622
|
+
if (!isSubWorkflowStep(step)) {
|
|
623
|
+
throw new Error(`Step ${step.id} is not a workflow step`);
|
|
312
624
|
}
|
|
313
625
|
// Resolve the sub-workflow path relative to the parent workflow
|
|
314
626
|
const subWorkflowPath = this.workflowPath
|
|
@@ -342,15 +654,302 @@ export class WorkflowEngine {
|
|
|
342
654
|
// Return the sub-workflow output
|
|
343
655
|
return result.output;
|
|
344
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Execute a sub-workflow using an AI sub-agent.
|
|
659
|
+
* The agent interprets the workflow and executes it autonomously.
|
|
660
|
+
*/
|
|
661
|
+
async executeSubWorkflowWithAgent(step, context, sdkRegistry, stepExecutor) {
|
|
662
|
+
// Resolve the sub-workflow path
|
|
663
|
+
const subWorkflowPath = this.workflowPath
|
|
664
|
+
? resolve(dirname(this.workflowPath), step.workflow)
|
|
665
|
+
: resolve(step.workflow);
|
|
666
|
+
// Read the workflow file content
|
|
667
|
+
const { readFile } = await import('node:fs/promises');
|
|
668
|
+
const workflowContent = await readFile(subWorkflowPath, 'utf-8');
|
|
669
|
+
// Resolve inputs for the sub-workflow
|
|
670
|
+
const resolvedInputs = resolveTemplates(step.inputs, context);
|
|
671
|
+
// Get subagent configuration
|
|
672
|
+
const subagentConfig = step.subagentConfig || {};
|
|
673
|
+
const model = subagentConfig.model || step.model || this.config.defaultModel;
|
|
674
|
+
const maxTurns = subagentConfig.maxTurns || 10;
|
|
675
|
+
const systemPrompt = subagentConfig.systemPrompt || this.buildDefaultSubagentSystemPrompt();
|
|
676
|
+
const tools = subagentConfig.tools || ['Read', 'Write', 'Bash', 'Glob', 'Grep'];
|
|
677
|
+
// Build the prompt for the agent
|
|
678
|
+
const agentPrompt = this.buildSubagentPrompt(workflowContent, resolvedInputs, tools);
|
|
679
|
+
// Determine the agent action to use
|
|
680
|
+
const agentName = step.agent || this.config.defaultAgent || 'agent';
|
|
681
|
+
const agentAction = `${agentName}.chat.completions`;
|
|
682
|
+
// Build the messages array
|
|
683
|
+
const messages = [
|
|
684
|
+
{ role: 'system', content: systemPrompt },
|
|
685
|
+
{ role: 'user', content: agentPrompt },
|
|
686
|
+
];
|
|
687
|
+
// Create a virtual action step to execute via the agent
|
|
688
|
+
const agentStep = {
|
|
689
|
+
id: `${step.id}-subagent`,
|
|
690
|
+
type: 'action',
|
|
691
|
+
action: agentAction,
|
|
692
|
+
inputs: {
|
|
693
|
+
model,
|
|
694
|
+
messages,
|
|
695
|
+
max_tokens: 8192,
|
|
696
|
+
},
|
|
697
|
+
model,
|
|
698
|
+
agent: agentName,
|
|
699
|
+
};
|
|
700
|
+
// Build executor context
|
|
701
|
+
const executorContext = this.buildStepExecutorContext(agentStep);
|
|
702
|
+
// Execute the agent call
|
|
703
|
+
let response;
|
|
704
|
+
let turns = 0;
|
|
705
|
+
let conversationMessages = [...messages];
|
|
706
|
+
let finalOutput = {};
|
|
707
|
+
while (turns < maxTurns) {
|
|
708
|
+
turns++;
|
|
709
|
+
try {
|
|
710
|
+
response = await stepExecutor({ ...agentStep, inputs: { ...agentStep.inputs, messages: conversationMessages } }, context, sdkRegistry, executorContext);
|
|
711
|
+
// Parse the response
|
|
712
|
+
const parsedResponse = this.parseSubagentResponse(response);
|
|
713
|
+
if (parsedResponse.completed) {
|
|
714
|
+
finalOutput = parsedResponse.output || {};
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
// If agent needs to continue, add its response and continue
|
|
718
|
+
if (parsedResponse.message) {
|
|
719
|
+
conversationMessages.push({ role: 'assistant', content: parsedResponse.message });
|
|
720
|
+
// Agent might request a tool call - for now, we'll prompt it to continue
|
|
721
|
+
conversationMessages.push({ role: 'user', content: 'Continue with the workflow execution.' });
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
// No clear continuation, assume completed
|
|
725
|
+
finalOutput = parsedResponse.output || {};
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
throw new Error(`Sub-agent execution failed at turn ${turns}: ${error instanceof Error ? error.message : String(error)}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (turns >= maxTurns) {
|
|
734
|
+
throw new Error(`Sub-agent exceeded maximum turns (${maxTurns})`);
|
|
735
|
+
}
|
|
736
|
+
return finalOutput;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Build the default system prompt for sub-agent execution.
|
|
740
|
+
*/
|
|
741
|
+
buildDefaultSubagentSystemPrompt() {
|
|
742
|
+
return `You are an AI agent executing a workflow. Your task is to interpret the workflow definition and execute each step in order.
|
|
743
|
+
|
|
744
|
+
For each step:
|
|
745
|
+
1. Understand what the step requires
|
|
746
|
+
2. Execute the action described
|
|
747
|
+
3. Store any outputs as specified
|
|
748
|
+
|
|
749
|
+
When you complete all steps, respond with a JSON object containing the workflow outputs.
|
|
750
|
+
|
|
751
|
+
Format your final response as:
|
|
752
|
+
\`\`\`json
|
|
753
|
+
{
|
|
754
|
+
"completed": true,
|
|
755
|
+
"output": { /* workflow outputs here */ }
|
|
756
|
+
}
|
|
757
|
+
\`\`\`
|
|
758
|
+
|
|
759
|
+
If you encounter an error, respond with:
|
|
760
|
+
\`\`\`json
|
|
761
|
+
{
|
|
762
|
+
"completed": false,
|
|
763
|
+
"error": "description of the error"
|
|
764
|
+
}
|
|
765
|
+
\`\`\``;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Build the prompt for sub-agent workflow execution.
|
|
769
|
+
*/
|
|
770
|
+
buildSubagentPrompt(workflowContent, inputs, tools) {
|
|
771
|
+
return `Execute the following workflow:
|
|
772
|
+
|
|
773
|
+
## Workflow Definition
|
|
774
|
+
\`\`\`markdown
|
|
775
|
+
${workflowContent}
|
|
776
|
+
\`\`\`
|
|
777
|
+
|
|
778
|
+
## Inputs
|
|
779
|
+
\`\`\`json
|
|
780
|
+
${JSON.stringify(inputs, null, 2)}
|
|
781
|
+
\`\`\`
|
|
782
|
+
|
|
783
|
+
## Available Tools
|
|
784
|
+
${tools.join(', ')}
|
|
785
|
+
|
|
786
|
+
Execute the workflow steps in order and return the final outputs as JSON.`;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Parse the sub-agent's response to extract completion status and output.
|
|
790
|
+
*/
|
|
791
|
+
parseSubagentResponse(response) {
|
|
792
|
+
// Try to extract content from various response formats
|
|
793
|
+
let content;
|
|
794
|
+
if (typeof response === 'string') {
|
|
795
|
+
content = response;
|
|
796
|
+
}
|
|
797
|
+
else if (response && typeof response === 'object') {
|
|
798
|
+
const resp = response;
|
|
799
|
+
// OpenAI-style response
|
|
800
|
+
if (resp.choices && Array.isArray(resp.choices)) {
|
|
801
|
+
const choice = resp.choices[0];
|
|
802
|
+
if (choice.message && typeof choice.message === 'object') {
|
|
803
|
+
const message = choice.message;
|
|
804
|
+
content = message.content;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
// Anthropic-style response
|
|
808
|
+
else if (resp.content && Array.isArray(resp.content)) {
|
|
809
|
+
const textBlock = resp.content.find((c) => typeof c === 'object' && c !== null && c.type === 'text');
|
|
810
|
+
content = textBlock?.text;
|
|
811
|
+
}
|
|
812
|
+
// Direct content field
|
|
813
|
+
else if (typeof resp.content === 'string') {
|
|
814
|
+
content = resp.content;
|
|
815
|
+
}
|
|
816
|
+
// Direct message field
|
|
817
|
+
else if (typeof resp.message === 'string') {
|
|
818
|
+
content = resp.message;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (!content) {
|
|
822
|
+
return { completed: false, message: 'No content in response' };
|
|
823
|
+
}
|
|
824
|
+
// Try to parse JSON from the response
|
|
825
|
+
const jsonMatch = content.match(/```json\n?([\s\S]*?)```/);
|
|
826
|
+
if (jsonMatch) {
|
|
827
|
+
try {
|
|
828
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
829
|
+
const output = parsed.output;
|
|
830
|
+
const error = parsed.error;
|
|
831
|
+
return {
|
|
832
|
+
completed: parsed.completed === true,
|
|
833
|
+
...(output !== undefined ? { output } : {}),
|
|
834
|
+
...(error !== undefined ? { error } : {}),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
catch (e) {
|
|
838
|
+
console.warn('[marktoflow] Failed to parse JSON block in sub-agent response:', e.message);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// Try to parse raw JSON
|
|
842
|
+
try {
|
|
843
|
+
const parsed = JSON.parse(content);
|
|
844
|
+
if (typeof parsed.completed === 'boolean') {
|
|
845
|
+
const output = parsed.output;
|
|
846
|
+
const error = parsed.error;
|
|
847
|
+
return {
|
|
848
|
+
completed: parsed.completed,
|
|
849
|
+
...(output !== undefined ? { output } : {}),
|
|
850
|
+
...(error !== undefined ? { error } : {}),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
catch (e) {
|
|
855
|
+
console.warn('[marktoflow] Sub-agent response is not valid JSON:', e.message);
|
|
856
|
+
}
|
|
857
|
+
// Return the content as a message
|
|
858
|
+
return { completed: false, message: content };
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Build the step executor context with effective model/agent/permissions.
|
|
862
|
+
*/
|
|
863
|
+
buildStepExecutorContext(step) {
|
|
864
|
+
// Merge workflow and step permissions
|
|
865
|
+
const effectivePermissions = mergePermissions(this.workflowPermissions, step.permissions);
|
|
866
|
+
// Resolve effective model/agent (step overrides workflow defaults)
|
|
867
|
+
const effectiveModel = step.model || this.config.defaultModel;
|
|
868
|
+
const effectiveAgent = step.agent || this.config.defaultAgent;
|
|
869
|
+
return {
|
|
870
|
+
model: effectiveModel,
|
|
871
|
+
agent: effectiveAgent,
|
|
872
|
+
permissions: effectivePermissions,
|
|
873
|
+
securityPolicy: toSecurityPolicy(effectivePermissions),
|
|
874
|
+
basePath: this.workflowPath,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Load and resolve an external prompt file for a step.
|
|
879
|
+
*/
|
|
880
|
+
async loadAndResolvePrompt(step, context) {
|
|
881
|
+
if (!step.prompt) {
|
|
882
|
+
return step.inputs;
|
|
883
|
+
}
|
|
884
|
+
// Check cache
|
|
885
|
+
let loadedPrompt = this.promptCache.get(step.prompt);
|
|
886
|
+
if (!loadedPrompt) {
|
|
887
|
+
loadedPrompt = await loadPromptFile(step.prompt, this.workflowPath);
|
|
888
|
+
this.promptCache.set(step.prompt, loadedPrompt);
|
|
889
|
+
}
|
|
890
|
+
// Resolve prompt inputs (from step.promptInputs, with template resolution)
|
|
891
|
+
const promptInputs = step.promptInputs
|
|
892
|
+
? resolveTemplates(step.promptInputs, context)
|
|
893
|
+
: {};
|
|
894
|
+
// Validate prompt inputs
|
|
895
|
+
const validation = validatePromptInputs(loadedPrompt, promptInputs);
|
|
896
|
+
if (!validation.valid) {
|
|
897
|
+
throw new Error(`Invalid prompt inputs: ${validation.errors.join(', ')}`);
|
|
898
|
+
}
|
|
899
|
+
// Resolve the prompt template
|
|
900
|
+
const resolved = resolvePromptTemplate(loadedPrompt, promptInputs, context);
|
|
901
|
+
// Merge resolved prompt content into inputs
|
|
902
|
+
// The resolved content typically goes into a 'messages' or 'prompt' field
|
|
903
|
+
const resolvedInputs = { ...step.inputs };
|
|
904
|
+
// If inputs has a 'messages' array with a user message, inject prompt content
|
|
905
|
+
if (Array.isArray(resolvedInputs.messages)) {
|
|
906
|
+
resolvedInputs.messages = resolvedInputs.messages.map((msg) => {
|
|
907
|
+
if (typeof msg === 'object' && msg !== null) {
|
|
908
|
+
const message = msg;
|
|
909
|
+
if (message.role === 'user' && typeof message.content === 'string') {
|
|
910
|
+
// Replace {{ prompt }} placeholder with resolved content
|
|
911
|
+
return {
|
|
912
|
+
...message,
|
|
913
|
+
content: message.content.replace(/\{\{\s*prompt\s*\}\}/g, resolved.content),
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return msg;
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
// Add resolved prompt as 'promptContent' for the executor to use
|
|
922
|
+
resolvedInputs.promptContent = resolved.content;
|
|
923
|
+
}
|
|
924
|
+
return resolvedInputs;
|
|
925
|
+
}
|
|
345
926
|
/**
|
|
346
927
|
* Execute a step with retry logic.
|
|
347
928
|
*/
|
|
348
929
|
async executeStepWithRetry(step, context, sdkRegistry, stepExecutor) {
|
|
349
|
-
const maxRetries = step.errorHandling?.maxRetries ?? this.config.maxRetries;
|
|
350
930
|
const startedAt = new Date();
|
|
351
931
|
let lastError;
|
|
932
|
+
// Build executor context with model/agent/permissions
|
|
933
|
+
const executorContext = this.buildStepExecutorContext(step);
|
|
352
934
|
// Handle sub-workflow execution
|
|
353
|
-
if (step
|
|
935
|
+
if (isSubWorkflowStep(step)) {
|
|
936
|
+
// Check if we should use subagent execution
|
|
937
|
+
if (step.useSubagent) {
|
|
938
|
+
try {
|
|
939
|
+
this.events.onStepStart?.(step, context);
|
|
940
|
+
const output = await this.executeWithTimeout(() => this.executeSubWorkflowWithAgent(step, context, sdkRegistry, stepExecutor), step.timeout ?? this.config.defaultTimeout);
|
|
941
|
+
const result = createStepResult(step.id, StepStatus.COMPLETED, output, startedAt, 0);
|
|
942
|
+
this.events.onStepComplete?.(step, result);
|
|
943
|
+
return result;
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
947
|
+
const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, lastError);
|
|
948
|
+
this.events.onStepComplete?.(step, result);
|
|
949
|
+
return result;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// Standard sub-workflow execution
|
|
354
953
|
try {
|
|
355
954
|
this.events.onStepStart?.(step, context);
|
|
356
955
|
const output = await this.executeWithTimeout(() => this.executeSubWorkflow(step, context, sdkRegistry, stepExecutor), step.timeout ?? this.config.defaultTimeout);
|
|
@@ -360,15 +959,17 @@ export class WorkflowEngine {
|
|
|
360
959
|
}
|
|
361
960
|
catch (error) {
|
|
362
961
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
363
|
-
const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, lastError
|
|
962
|
+
const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, lastError // Pass full error object
|
|
963
|
+
);
|
|
364
964
|
this.events.onStepComplete?.(step, result);
|
|
365
965
|
return result;
|
|
366
966
|
}
|
|
367
967
|
}
|
|
368
968
|
// Regular action step - ensure action is defined
|
|
369
|
-
if (!step
|
|
370
|
-
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Step
|
|
969
|
+
if (!isActionStep(step)) {
|
|
970
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Step is neither an action nor a workflow');
|
|
371
971
|
}
|
|
972
|
+
const maxRetries = step.errorHandling?.maxRetries ?? this.config.maxRetries;
|
|
372
973
|
// Get or create circuit breaker for this step's action
|
|
373
974
|
const [serviceName] = step.action.split('.');
|
|
374
975
|
let circuitBreaker = this.circuitBreakers.get(serviceName);
|
|
@@ -383,11 +984,30 @@ export class WorkflowEngine {
|
|
|
383
984
|
}
|
|
384
985
|
this.events.onStepStart?.(step, context);
|
|
385
986
|
try {
|
|
386
|
-
//
|
|
387
|
-
|
|
987
|
+
// Load and resolve external prompt if specified
|
|
988
|
+
let resolvedInputs;
|
|
989
|
+
if (step.prompt) {
|
|
990
|
+
resolvedInputs = await this.loadAndResolvePrompt(step, context);
|
|
991
|
+
resolvedInputs = resolveTemplates(resolvedInputs, context);
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
// Resolve templates in inputs
|
|
995
|
+
resolvedInputs = resolveTemplates(step.inputs, context);
|
|
996
|
+
}
|
|
388
997
|
const stepWithResolvedInputs = { ...step, inputs: resolvedInputs };
|
|
389
|
-
//
|
|
390
|
-
|
|
998
|
+
// Check if this is a built-in operation
|
|
999
|
+
let output;
|
|
1000
|
+
if (isBuiltInOperation(step.action)) {
|
|
1001
|
+
// Execute built-in operation directly (no timeout, no SDK executor needed)
|
|
1002
|
+
// For built-in operations, pass both resolved and unresolved inputs
|
|
1003
|
+
// to allow selective resolution of template expressions
|
|
1004
|
+
// Await in case operation is async (e.g., file operations)
|
|
1005
|
+
output = await executeBuiltInOperation(step.action, step.inputs, resolvedInputs, context);
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
// Execute step with executor context
|
|
1009
|
+
output = await this.executeWithTimeout(() => stepExecutor(stepWithResolvedInputs, context, sdkRegistry, executorContext), step.timeout ?? this.config.defaultTimeout);
|
|
1010
|
+
}
|
|
391
1011
|
circuitBreaker.recordSuccess();
|
|
392
1012
|
const result = createStepResult(step.id, StepStatus.COMPLETED, output, startedAt, attempt);
|
|
393
1013
|
this.events.onStepComplete?.(step, result);
|
|
@@ -405,7 +1025,8 @@ export class WorkflowEngine {
|
|
|
405
1025
|
}
|
|
406
1026
|
}
|
|
407
1027
|
// All retries exhausted
|
|
408
|
-
const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, maxRetries, lastError
|
|
1028
|
+
const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, maxRetries, lastError // Pass full error object to preserve HTTP details, stack traces, etc.
|
|
1029
|
+
);
|
|
409
1030
|
this.events.onStepComplete?.(step, result);
|
|
410
1031
|
return result;
|
|
411
1032
|
}
|
|
@@ -414,8 +1035,8 @@ export class WorkflowEngine {
|
|
|
414
1035
|
*/
|
|
415
1036
|
async executeStepWithFailover(step, context, sdkRegistry, stepExecutor) {
|
|
416
1037
|
const primaryResult = await this.executeStepWithRetry(step, context, sdkRegistry, stepExecutor);
|
|
417
|
-
// Sub-workflows don't support failover
|
|
418
|
-
if (
|
|
1038
|
+
// Sub-workflows and non-action steps don't support failover
|
|
1039
|
+
if (!isActionStep(step)) {
|
|
419
1040
|
return primaryResult;
|
|
420
1041
|
}
|
|
421
1042
|
const [primaryTool, ...methodParts] = step.action.split('.');
|
|
@@ -424,7 +1045,7 @@ export class WorkflowEngine {
|
|
|
424
1045
|
this.healthTracker.markHealthy(primaryTool);
|
|
425
1046
|
return primaryResult;
|
|
426
1047
|
}
|
|
427
|
-
const errorMessage = primaryResult.error
|
|
1048
|
+
const errorMessage = primaryResult.error ? errorToString(primaryResult.error) : '';
|
|
428
1049
|
const isTimeout = errorMessage.includes('timed out');
|
|
429
1050
|
if (isTimeout && !this.failoverConfig.failoverOnTimeout) {
|
|
430
1051
|
this.healthTracker.markUnhealthy(primaryTool, errorMessage);
|
|
@@ -444,7 +1065,7 @@ export class WorkflowEngine {
|
|
|
444
1065
|
continue;
|
|
445
1066
|
if (attempts >= this.failoverConfig.maxFailoverAttempts)
|
|
446
1067
|
break;
|
|
447
|
-
const fallbackStep = { ...step, action: `${fallbackTool}.${method}
|
|
1068
|
+
const fallbackStep = { ...step, action: `${fallbackTool}.${method}`, type: 'action' };
|
|
448
1069
|
const result = await this.executeStepWithRetry(fallbackStep, context, sdkRegistry, stepExecutor);
|
|
449
1070
|
this.failoverEvents.push({
|
|
450
1071
|
timestamp: new Date(),
|
|
@@ -528,12 +1149,27 @@ export class WorkflowEngine {
|
|
|
528
1149
|
/**
|
|
529
1150
|
* Resolve a condition value with support for nested properties.
|
|
530
1151
|
* Handles direct variable references and nested paths.
|
|
1152
|
+
* Uses Nunjucks for template expressions with filters/regex.
|
|
531
1153
|
*/
|
|
532
1154
|
resolveConditionValue(path, context) {
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1155
|
+
// If it looks like a template expression, resolve it
|
|
1156
|
+
if (path.includes('|') || path.includes('=~') || path.includes('!~')) {
|
|
1157
|
+
// Build template context
|
|
1158
|
+
const templateContext = {
|
|
1159
|
+
inputs: context.inputs,
|
|
1160
|
+
...context.variables,
|
|
1161
|
+
};
|
|
1162
|
+
return renderTemplate(`{{ ${path} }}`, templateContext);
|
|
1163
|
+
}
|
|
1164
|
+
// First try to parse as a literal value (true, false, numbers, etc.)
|
|
1165
|
+
const parsedValue = this.parseValue(path);
|
|
1166
|
+
// If parseValue returned the same string, try to resolve as a variable
|
|
1167
|
+
if (parsedValue === path) {
|
|
1168
|
+
const resolved = resolveVariablePath(path, context);
|
|
1169
|
+
return resolved;
|
|
1170
|
+
}
|
|
1171
|
+
// Return the parsed literal value
|
|
1172
|
+
return parsedValue;
|
|
537
1173
|
}
|
|
538
1174
|
/**
|
|
539
1175
|
* Parse a value from a condition string.
|
|
@@ -562,12 +1198,14 @@ export class WorkflowEngine {
|
|
|
562
1198
|
*/
|
|
563
1199
|
buildWorkflowResult(workflow, context, stepResults, startedAt, error) {
|
|
564
1200
|
const completedAt = new Date();
|
|
1201
|
+
// Use workflowOutputs if set by workflow.set_outputs, otherwise use all variables
|
|
1202
|
+
const output = context.workflowOutputs || context.variables;
|
|
565
1203
|
return {
|
|
566
1204
|
workflowId: workflow.metadata.id,
|
|
567
1205
|
runId: context.runId,
|
|
568
1206
|
status: context.status,
|
|
569
1207
|
stepResults,
|
|
570
|
-
output
|
|
1208
|
+
output,
|
|
571
1209
|
error,
|
|
572
1210
|
startedAt,
|
|
573
1211
|
completedAt,
|
|
@@ -582,6 +1220,704 @@ export class WorkflowEngine {
|
|
|
582
1220
|
breaker.reset();
|
|
583
1221
|
}
|
|
584
1222
|
}
|
|
1223
|
+
// ============================================================================
|
|
1224
|
+
// Control Flow Execution Methods
|
|
1225
|
+
// ============================================================================
|
|
1226
|
+
/**
|
|
1227
|
+
* Execute an if/else conditional step.
|
|
1228
|
+
*/
|
|
1229
|
+
async executeIfStep(step, context, sdkRegistry, stepExecutor) {
|
|
1230
|
+
const startedAt = new Date();
|
|
1231
|
+
try {
|
|
1232
|
+
// Evaluate condition
|
|
1233
|
+
const conditionResult = this.evaluateCondition(step.condition, context);
|
|
1234
|
+
// Determine which branch to execute
|
|
1235
|
+
const branchSteps = conditionResult
|
|
1236
|
+
? step.then || step.steps // 'steps' is alias for 'then'
|
|
1237
|
+
: step.else;
|
|
1238
|
+
if (!branchSteps || branchSteps.length === 0) {
|
|
1239
|
+
return createStepResult(step.id, StepStatus.SKIPPED, null, startedAt);
|
|
1240
|
+
}
|
|
1241
|
+
// Execute the branch steps
|
|
1242
|
+
const branchResults = [];
|
|
1243
|
+
for (const branchStep of branchSteps) {
|
|
1244
|
+
const result = await this.executeStep(branchStep, context, sdkRegistry, stepExecutor);
|
|
1245
|
+
if (result.status === StepStatus.COMPLETED && branchStep.outputVariable) {
|
|
1246
|
+
context.variables[branchStep.outputVariable] = result.output;
|
|
1247
|
+
branchResults.push(result.output);
|
|
1248
|
+
}
|
|
1249
|
+
if (result.status === StepStatus.FAILED) {
|
|
1250
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return createStepResult(step.id, StepStatus.COMPLETED, branchResults, startedAt);
|
|
1254
|
+
}
|
|
1255
|
+
catch (error) {
|
|
1256
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Execute a switch/case step.
|
|
1261
|
+
*/
|
|
1262
|
+
async executeSwitchStep(step, context, sdkRegistry, stepExecutor) {
|
|
1263
|
+
const startedAt = new Date();
|
|
1264
|
+
try {
|
|
1265
|
+
// Resolve the switch expression
|
|
1266
|
+
const expressionValue = String(resolveTemplates(step.expression, context));
|
|
1267
|
+
// Find matching case
|
|
1268
|
+
const caseSteps = step.cases[expressionValue] || step.default;
|
|
1269
|
+
if (!caseSteps || caseSteps.length === 0) {
|
|
1270
|
+
return createStepResult(step.id, StepStatus.SKIPPED, null, startedAt);
|
|
1271
|
+
}
|
|
1272
|
+
// Execute case steps
|
|
1273
|
+
const caseResults = [];
|
|
1274
|
+
for (const caseStep of caseSteps) {
|
|
1275
|
+
const result = await this.executeStep(caseStep, context, sdkRegistry, stepExecutor);
|
|
1276
|
+
if (result.status === StepStatus.COMPLETED && caseStep.outputVariable) {
|
|
1277
|
+
context.variables[caseStep.outputVariable] = result.output;
|
|
1278
|
+
caseResults.push(result.output);
|
|
1279
|
+
}
|
|
1280
|
+
if (result.status === StepStatus.FAILED) {
|
|
1281
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return createStepResult(step.id, StepStatus.COMPLETED, caseResults, startedAt);
|
|
1285
|
+
}
|
|
1286
|
+
catch (error) {
|
|
1287
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Execute a for-each loop step.
|
|
1292
|
+
* Supports optional batch_size and pause_between_batches for rate limiting.
|
|
1293
|
+
*/
|
|
1294
|
+
async executeForEachStep(step, context, sdkRegistry, stepExecutor) {
|
|
1295
|
+
const startedAt = new Date();
|
|
1296
|
+
const cleanupLoopVars = () => {
|
|
1297
|
+
delete context.variables[step.itemVariable];
|
|
1298
|
+
delete context.variables['loop'];
|
|
1299
|
+
delete context.variables['batch'];
|
|
1300
|
+
if (step.indexVariable)
|
|
1301
|
+
delete context.variables[step.indexVariable];
|
|
1302
|
+
};
|
|
1303
|
+
try {
|
|
1304
|
+
// Resolve items array
|
|
1305
|
+
const items = resolveTemplates(step.items, context);
|
|
1306
|
+
if (!Array.isArray(items)) {
|
|
1307
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
|
|
1308
|
+
}
|
|
1309
|
+
if (items.length === 0) {
|
|
1310
|
+
return createStepResult(step.id, StepStatus.SKIPPED, [], startedAt);
|
|
1311
|
+
}
|
|
1312
|
+
const batchSize = step.batchSize;
|
|
1313
|
+
const pauseBetweenBatches = step.pauseBetweenBatches;
|
|
1314
|
+
// If batch mode, process items in batches
|
|
1315
|
+
if (batchSize && batchSize > 0) {
|
|
1316
|
+
return await this.executeForEachBatched(step, items, batchSize, pauseBetweenBatches ?? 0, context, sdkRegistry, stepExecutor, startedAt);
|
|
1317
|
+
}
|
|
1318
|
+
// Standard item-by-item execution
|
|
1319
|
+
const results = [];
|
|
1320
|
+
for (let i = 0; i < items.length; i++) {
|
|
1321
|
+
context.variables[step.itemVariable] = items[i];
|
|
1322
|
+
context.variables['loop'] = {
|
|
1323
|
+
index: i,
|
|
1324
|
+
first: i === 0,
|
|
1325
|
+
last: i === items.length - 1,
|
|
1326
|
+
length: items.length,
|
|
1327
|
+
};
|
|
1328
|
+
if (step.indexVariable) {
|
|
1329
|
+
context.variables[step.indexVariable] = i;
|
|
1330
|
+
}
|
|
1331
|
+
// Execute iteration steps
|
|
1332
|
+
for (const iterStep of step.steps) {
|
|
1333
|
+
const result = await this.executeStep(iterStep, context, sdkRegistry, stepExecutor);
|
|
1334
|
+
if (result.status === StepStatus.COMPLETED && iterStep.outputVariable) {
|
|
1335
|
+
context.variables[iterStep.outputVariable] = result.output;
|
|
1336
|
+
}
|
|
1337
|
+
if (result.status === StepStatus.FAILED) {
|
|
1338
|
+
const errorAction = step.errorHandling?.action ?? 'stop';
|
|
1339
|
+
if (errorAction === 'stop') {
|
|
1340
|
+
cleanupLoopVars();
|
|
1341
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
1342
|
+
}
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
results.push(context.variables[step.itemVariable]);
|
|
1347
|
+
}
|
|
1348
|
+
cleanupLoopVars();
|
|
1349
|
+
return createStepResult(step.id, StepStatus.COMPLETED, results, startedAt);
|
|
1350
|
+
}
|
|
1351
|
+
catch (error) {
|
|
1352
|
+
cleanupLoopVars();
|
|
1353
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Execute for-each in batch mode.
|
|
1358
|
+
* Items are split into batches; {{ batch }} contains the current batch array.
|
|
1359
|
+
*/
|
|
1360
|
+
async executeForEachBatched(step, items, batchSize, pauseBetweenBatches, context, sdkRegistry, stepExecutor, startedAt) {
|
|
1361
|
+
const results = [];
|
|
1362
|
+
const totalBatches = Math.ceil(items.length / batchSize);
|
|
1363
|
+
for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
|
|
1364
|
+
const batchStart = batchIndex * batchSize;
|
|
1365
|
+
const batchItems = items.slice(batchStart, batchStart + batchSize);
|
|
1366
|
+
// Pause between batches (not before first batch)
|
|
1367
|
+
if (batchIndex > 0 && pauseBetweenBatches > 0) {
|
|
1368
|
+
await new Promise((resolve) => setTimeout(resolve, pauseBetweenBatches));
|
|
1369
|
+
}
|
|
1370
|
+
// Expose batch-level variables
|
|
1371
|
+
context.variables['batch'] = batchItems;
|
|
1372
|
+
context.variables['loop'] = {
|
|
1373
|
+
index: batchIndex,
|
|
1374
|
+
first: batchIndex === 0,
|
|
1375
|
+
last: batchIndex === totalBatches - 1,
|
|
1376
|
+
length: totalBatches,
|
|
1377
|
+
batchSize,
|
|
1378
|
+
batchStart,
|
|
1379
|
+
totalItems: items.length,
|
|
1380
|
+
};
|
|
1381
|
+
// Process each item in the batch
|
|
1382
|
+
for (let i = 0; i < batchItems.length; i++) {
|
|
1383
|
+
const globalIndex = batchStart + i;
|
|
1384
|
+
context.variables[step.itemVariable] = batchItems[i];
|
|
1385
|
+
if (step.indexVariable) {
|
|
1386
|
+
context.variables[step.indexVariable] = globalIndex;
|
|
1387
|
+
}
|
|
1388
|
+
for (const iterStep of step.steps) {
|
|
1389
|
+
const result = await this.executeStep(iterStep, context, sdkRegistry, stepExecutor);
|
|
1390
|
+
if (result.status === StepStatus.COMPLETED && iterStep.outputVariable) {
|
|
1391
|
+
context.variables[iterStep.outputVariable] = result.output;
|
|
1392
|
+
}
|
|
1393
|
+
if (result.status === StepStatus.FAILED) {
|
|
1394
|
+
const errorAction = step.errorHandling?.action ?? 'stop';
|
|
1395
|
+
if (errorAction === 'stop') {
|
|
1396
|
+
delete context.variables[step.itemVariable];
|
|
1397
|
+
delete context.variables['loop'];
|
|
1398
|
+
delete context.variables['batch'];
|
|
1399
|
+
if (step.indexVariable)
|
|
1400
|
+
delete context.variables[step.indexVariable];
|
|
1401
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
1402
|
+
}
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
results.push(context.variables[step.itemVariable]);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
delete context.variables[step.itemVariable];
|
|
1410
|
+
delete context.variables['loop'];
|
|
1411
|
+
delete context.variables['batch'];
|
|
1412
|
+
if (step.indexVariable)
|
|
1413
|
+
delete context.variables[step.indexVariable];
|
|
1414
|
+
return createStepResult(step.id, StepStatus.COMPLETED, results, startedAt);
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Execute a while loop step.
|
|
1418
|
+
*/
|
|
1419
|
+
async executeWhileStep(step, context, sdkRegistry, stepExecutor) {
|
|
1420
|
+
const startedAt = new Date();
|
|
1421
|
+
let iterations = 0;
|
|
1422
|
+
try {
|
|
1423
|
+
while (this.evaluateCondition(step.condition, context)) {
|
|
1424
|
+
if (iterations >= step.maxIterations) {
|
|
1425
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, `Max iterations (${step.maxIterations}) exceeded`);
|
|
1426
|
+
}
|
|
1427
|
+
// Execute iteration steps
|
|
1428
|
+
for (const iterStep of step.steps) {
|
|
1429
|
+
const result = await this.executeStep(iterStep, context, sdkRegistry, stepExecutor);
|
|
1430
|
+
if (result.status === StepStatus.COMPLETED && iterStep.outputVariable) {
|
|
1431
|
+
context.variables[iterStep.outputVariable] = result.output;
|
|
1432
|
+
}
|
|
1433
|
+
if (result.status === StepStatus.FAILED) {
|
|
1434
|
+
const errorAction = step.errorHandling?.action ?? 'stop';
|
|
1435
|
+
if (errorAction === 'stop') {
|
|
1436
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
1437
|
+
}
|
|
1438
|
+
// 'continue' - skip to next iteration
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
iterations++;
|
|
1443
|
+
}
|
|
1444
|
+
return createStepResult(step.id, StepStatus.COMPLETED, { iterations }, startedAt);
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Execute a map transformation step.
|
|
1452
|
+
*/
|
|
1453
|
+
async executeMapStep(step, context, _sdkRegistry, _stepExecutor) {
|
|
1454
|
+
const startedAt = new Date();
|
|
1455
|
+
try {
|
|
1456
|
+
// Resolve items array
|
|
1457
|
+
const items = resolveTemplates(step.items, context);
|
|
1458
|
+
if (!Array.isArray(items)) {
|
|
1459
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
|
|
1460
|
+
}
|
|
1461
|
+
// Map each item using the expression
|
|
1462
|
+
const mapped = items.map((item) => {
|
|
1463
|
+
context.variables[step.itemVariable] = item;
|
|
1464
|
+
const result = resolveTemplates(step.expression, context);
|
|
1465
|
+
delete context.variables[step.itemVariable];
|
|
1466
|
+
return result;
|
|
1467
|
+
});
|
|
1468
|
+
return createStepResult(step.id, StepStatus.COMPLETED, mapped, startedAt);
|
|
1469
|
+
}
|
|
1470
|
+
catch (error) {
|
|
1471
|
+
delete context.variables[step.itemVariable];
|
|
1472
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Execute a filter step.
|
|
1477
|
+
*/
|
|
1478
|
+
async executeFilterStep(step, context, _sdkRegistry, _stepExecutor) {
|
|
1479
|
+
const startedAt = new Date();
|
|
1480
|
+
try {
|
|
1481
|
+
// Resolve items array
|
|
1482
|
+
const items = resolveTemplates(step.items, context);
|
|
1483
|
+
if (!Array.isArray(items)) {
|
|
1484
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
|
|
1485
|
+
}
|
|
1486
|
+
// Filter items using the condition
|
|
1487
|
+
const filtered = items.filter((item) => {
|
|
1488
|
+
context.variables[step.itemVariable] = item;
|
|
1489
|
+
const result = this.evaluateCondition(step.condition, context);
|
|
1490
|
+
delete context.variables[step.itemVariable];
|
|
1491
|
+
return result;
|
|
1492
|
+
});
|
|
1493
|
+
return createStepResult(step.id, StepStatus.COMPLETED, filtered, startedAt);
|
|
1494
|
+
}
|
|
1495
|
+
catch (error) {
|
|
1496
|
+
delete context.variables[step.itemVariable];
|
|
1497
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Execute a reduce/aggregate step.
|
|
1502
|
+
*/
|
|
1503
|
+
async executeReduceStep(step, context, _sdkRegistry, _stepExecutor) {
|
|
1504
|
+
const startedAt = new Date();
|
|
1505
|
+
try {
|
|
1506
|
+
// Resolve items array
|
|
1507
|
+
const items = resolveTemplates(step.items, context);
|
|
1508
|
+
if (!Array.isArray(items)) {
|
|
1509
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
|
|
1510
|
+
}
|
|
1511
|
+
// Reduce items using the expression
|
|
1512
|
+
let accumulator = step.initialValue ?? null;
|
|
1513
|
+
for (const item of items) {
|
|
1514
|
+
context.variables[step.itemVariable] = item;
|
|
1515
|
+
context.variables[step.accumulatorVariable] = accumulator;
|
|
1516
|
+
accumulator = resolveTemplates(step.expression, context);
|
|
1517
|
+
delete context.variables[step.itemVariable];
|
|
1518
|
+
delete context.variables[step.accumulatorVariable];
|
|
1519
|
+
}
|
|
1520
|
+
return createStepResult(step.id, StepStatus.COMPLETED, accumulator, startedAt);
|
|
1521
|
+
}
|
|
1522
|
+
catch (error) {
|
|
1523
|
+
delete context.variables[step.itemVariable];
|
|
1524
|
+
delete context.variables[step.accumulatorVariable];
|
|
1525
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Execute parallel branches.
|
|
1530
|
+
*/
|
|
1531
|
+
async executeParallelStep(step, context, sdkRegistry, stepExecutor) {
|
|
1532
|
+
const startedAt = new Date();
|
|
1533
|
+
try {
|
|
1534
|
+
// Execute branches in parallel
|
|
1535
|
+
const branchPromises = step.branches.map(async (branch) => {
|
|
1536
|
+
// Clone context for isolation
|
|
1537
|
+
const branchContext = this.cloneContext(context);
|
|
1538
|
+
// Execute branch steps
|
|
1539
|
+
const branchResults = [];
|
|
1540
|
+
for (const branchStep of branch.steps) {
|
|
1541
|
+
const result = await this.executeStep(branchStep, branchContext, sdkRegistry, stepExecutor);
|
|
1542
|
+
if (result.status === StepStatus.COMPLETED && branchStep.outputVariable) {
|
|
1543
|
+
branchContext.variables[branchStep.outputVariable] = result.output;
|
|
1544
|
+
branchResults.push(result.output);
|
|
1545
|
+
}
|
|
1546
|
+
if (result.status === StepStatus.FAILED) {
|
|
1547
|
+
throw new Error(`Branch ${branch.id} failed: ${errorToString(result.error)}`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return { branchId: branch.id, context: branchContext, results: branchResults };
|
|
1551
|
+
});
|
|
1552
|
+
// Wait for all branches (or limited concurrency)
|
|
1553
|
+
const branchResults = step.maxConcurrent
|
|
1554
|
+
? await this.executeConcurrentlyWithLimit(branchPromises, step.maxConcurrent)
|
|
1555
|
+
: await Promise.all(branchPromises);
|
|
1556
|
+
// Merge branch contexts back into main context
|
|
1557
|
+
for (const { branchId, context: branchContext } of branchResults) {
|
|
1558
|
+
this.mergeContexts(context, branchContext, branchId);
|
|
1559
|
+
}
|
|
1560
|
+
const outputs = branchResults.map((br) => br.results);
|
|
1561
|
+
return createStepResult(step.id, StepStatus.COMPLETED, outputs, startedAt);
|
|
1562
|
+
}
|
|
1563
|
+
catch (error) {
|
|
1564
|
+
if (step.onError === 'continue') {
|
|
1565
|
+
return createStepResult(step.id, StepStatus.COMPLETED, null, startedAt);
|
|
1566
|
+
}
|
|
1567
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Execute try/catch/finally step.
|
|
1572
|
+
*/
|
|
1573
|
+
async executeTryStep(step, context, sdkRegistry, stepExecutor) {
|
|
1574
|
+
const startedAt = new Date();
|
|
1575
|
+
let tryError;
|
|
1576
|
+
try {
|
|
1577
|
+
// Execute try block
|
|
1578
|
+
for (const tryStep of step.try) {
|
|
1579
|
+
const result = await this.executeStep(tryStep, context, sdkRegistry, stepExecutor);
|
|
1580
|
+
if (result.status === StepStatus.COMPLETED && tryStep.outputVariable) {
|
|
1581
|
+
context.variables[tryStep.outputVariable] = result.output;
|
|
1582
|
+
}
|
|
1583
|
+
if (result.status === StepStatus.FAILED) {
|
|
1584
|
+
tryError = new Error(result.error ? errorToString(result.error) : 'Step failed');
|
|
1585
|
+
break;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// If error occurred and catch block exists, execute catch
|
|
1589
|
+
let catchError;
|
|
1590
|
+
if (tryError && step.catch) {
|
|
1591
|
+
// Inject error object into context
|
|
1592
|
+
context.variables['error'] = {
|
|
1593
|
+
message: tryError.message,
|
|
1594
|
+
step: tryError,
|
|
1595
|
+
};
|
|
1596
|
+
for (const catchStep of step.catch) {
|
|
1597
|
+
const result = await this.executeStep(catchStep, context, sdkRegistry, stepExecutor);
|
|
1598
|
+
if (result.status === StepStatus.COMPLETED && catchStep.outputVariable) {
|
|
1599
|
+
context.variables[catchStep.outputVariable] = result.output;
|
|
1600
|
+
}
|
|
1601
|
+
if (result.status === StepStatus.FAILED) {
|
|
1602
|
+
catchError = new Error(result.error ? errorToString(result.error) : 'Catch block failed');
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
delete context.variables['error'];
|
|
1607
|
+
}
|
|
1608
|
+
// Execute finally block (always runs)
|
|
1609
|
+
if (step.finally) {
|
|
1610
|
+
for (const finallyStep of step.finally) {
|
|
1611
|
+
const result = await this.executeStep(finallyStep, context, sdkRegistry, stepExecutor);
|
|
1612
|
+
if (result.status === StepStatus.COMPLETED && finallyStep.outputVariable) {
|
|
1613
|
+
context.variables[finallyStep.outputVariable] = result.output;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
// Return success if catch handled the error, or error if not
|
|
1618
|
+
if (tryError && !step.catch) {
|
|
1619
|
+
// No catch block to handle error
|
|
1620
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, tryError.message);
|
|
1621
|
+
}
|
|
1622
|
+
if (catchError) {
|
|
1623
|
+
// Catch block also failed
|
|
1624
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, catchError.message);
|
|
1625
|
+
}
|
|
1626
|
+
return createStepResult(step.id, StepStatus.COMPLETED, null, startedAt);
|
|
1627
|
+
}
|
|
1628
|
+
catch (error) {
|
|
1629
|
+
// Execute finally even on unexpected error
|
|
1630
|
+
if (step.finally) {
|
|
1631
|
+
try {
|
|
1632
|
+
for (const finallyStep of step.finally) {
|
|
1633
|
+
const result = await this.executeStep(finallyStep, context, sdkRegistry, stepExecutor);
|
|
1634
|
+
if (result.status === StepStatus.COMPLETED && finallyStep.outputVariable) {
|
|
1635
|
+
context.variables[finallyStep.outputVariable] = result.output;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
catch {
|
|
1640
|
+
// Ignore finally errors
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Execute a script step (inline JavaScript).
|
|
1648
|
+
*/
|
|
1649
|
+
async executeScriptStep(step, context) {
|
|
1650
|
+
const startedAt = new Date();
|
|
1651
|
+
try {
|
|
1652
|
+
// Resolve any templates in the code
|
|
1653
|
+
const resolvedInputs = resolveTemplates(step.inputs, context);
|
|
1654
|
+
// Execute the script with the workflow context
|
|
1655
|
+
const result = await executeScriptAsync(resolvedInputs.code, {
|
|
1656
|
+
variables: context.variables,
|
|
1657
|
+
inputs: context.inputs,
|
|
1658
|
+
steps: context.stepMetadata,
|
|
1659
|
+
}, {
|
|
1660
|
+
timeout: resolvedInputs.timeout ?? 5000,
|
|
1661
|
+
});
|
|
1662
|
+
if (!result.success) {
|
|
1663
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error ?? 'Script execution failed');
|
|
1664
|
+
}
|
|
1665
|
+
return createStepResult(step.id, StepStatus.COMPLETED, result.value, startedAt);
|
|
1666
|
+
}
|
|
1667
|
+
catch (error) {
|
|
1668
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
// ============================================================================
|
|
1672
|
+
// Wait Step Execution
|
|
1673
|
+
// ============================================================================
|
|
1674
|
+
/**
|
|
1675
|
+
* Execute a wait/pause step.
|
|
1676
|
+
*
|
|
1677
|
+
* For mode=duration: In-process wait (suitable for short durations).
|
|
1678
|
+
* For persistent long-duration waits, a WaitManager should checkpoint
|
|
1679
|
+
* the execution and resume it later via the scheduler.
|
|
1680
|
+
*
|
|
1681
|
+
* For mode=webhook: Returns a resume URL. The execution will be
|
|
1682
|
+
* checkpointed and resumed when the URL is called.
|
|
1683
|
+
*
|
|
1684
|
+
* For mode=form: Returns form fields. The execution will be
|
|
1685
|
+
* checkpointed and resumed when the form is submitted.
|
|
1686
|
+
*/
|
|
1687
|
+
async executeWaitStep(step, context) {
|
|
1688
|
+
const startedAt = new Date();
|
|
1689
|
+
try {
|
|
1690
|
+
switch (step.mode) {
|
|
1691
|
+
case 'duration': {
|
|
1692
|
+
if (!step.duration) {
|
|
1693
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Wait step with mode=duration requires a duration');
|
|
1694
|
+
}
|
|
1695
|
+
const resolvedDuration = resolveTemplates(step.duration, context);
|
|
1696
|
+
const ms = parseDuration(resolvedDuration);
|
|
1697
|
+
// For short durations (under 5 minutes), do in-process wait
|
|
1698
|
+
if (ms <= 300000) {
|
|
1699
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1700
|
+
return createStepResult(step.id, StepStatus.COMPLETED, { waited: ms }, startedAt);
|
|
1701
|
+
}
|
|
1702
|
+
// For longer durations, checkpoint and schedule resume
|
|
1703
|
+
// The StateStore will persist the execution state
|
|
1704
|
+
if (this.stateStore) {
|
|
1705
|
+
this.stateStore.saveCheckpoint({
|
|
1706
|
+
runId: context.runId,
|
|
1707
|
+
stepIndex: context.currentStepIndex,
|
|
1708
|
+
stepName: step.id,
|
|
1709
|
+
status: StepStatus.COMPLETED,
|
|
1710
|
+
startedAt: startedAt,
|
|
1711
|
+
completedAt: new Date(),
|
|
1712
|
+
inputs: { mode: 'duration', resumeAt: new Date(Date.now() + ms).toISOString() },
|
|
1713
|
+
outputs: { waiting: true },
|
|
1714
|
+
error: null,
|
|
1715
|
+
retryCount: 0,
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
// Set execution status to indicate waiting
|
|
1719
|
+
const resumeAt = new Date(Date.now() + ms).toISOString();
|
|
1720
|
+
return createStepResult(step.id, StepStatus.COMPLETED, {
|
|
1721
|
+
waiting: true,
|
|
1722
|
+
mode: 'duration',
|
|
1723
|
+
resumeAt,
|
|
1724
|
+
durationMs: ms,
|
|
1725
|
+
}, startedAt);
|
|
1726
|
+
}
|
|
1727
|
+
case 'webhook': {
|
|
1728
|
+
// Generate a unique resume URL
|
|
1729
|
+
const resumeToken = crypto.randomUUID();
|
|
1730
|
+
const webhookPath = step.webhookPath
|
|
1731
|
+
? resolveTemplates(step.webhookPath, context)
|
|
1732
|
+
: `/resume/${context.runId}/${step.id}/${resumeToken}`;
|
|
1733
|
+
if (this.stateStore) {
|
|
1734
|
+
this.stateStore.saveCheckpoint({
|
|
1735
|
+
runId: context.runId,
|
|
1736
|
+
stepIndex: context.currentStepIndex,
|
|
1737
|
+
stepName: step.id,
|
|
1738
|
+
status: StepStatus.COMPLETED,
|
|
1739
|
+
startedAt: startedAt,
|
|
1740
|
+
completedAt: new Date(),
|
|
1741
|
+
inputs: { mode: 'webhook', resumeToken, webhookPath },
|
|
1742
|
+
outputs: { waiting: true },
|
|
1743
|
+
error: null,
|
|
1744
|
+
retryCount: 0,
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
return createStepResult(step.id, StepStatus.COMPLETED, {
|
|
1748
|
+
waiting: true,
|
|
1749
|
+
mode: 'webhook',
|
|
1750
|
+
resumeToken,
|
|
1751
|
+
webhookPath,
|
|
1752
|
+
}, startedAt);
|
|
1753
|
+
}
|
|
1754
|
+
case 'form': {
|
|
1755
|
+
if (!step.fields || Object.keys(step.fields).length === 0) {
|
|
1756
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Wait step with mode=form requires fields');
|
|
1757
|
+
}
|
|
1758
|
+
const resumeToken = crypto.randomUUID();
|
|
1759
|
+
if (this.stateStore) {
|
|
1760
|
+
this.stateStore.saveCheckpoint({
|
|
1761
|
+
runId: context.runId,
|
|
1762
|
+
stepIndex: context.currentStepIndex,
|
|
1763
|
+
stepName: step.id,
|
|
1764
|
+
status: StepStatus.COMPLETED,
|
|
1765
|
+
startedAt: startedAt,
|
|
1766
|
+
completedAt: new Date(),
|
|
1767
|
+
inputs: { mode: 'form', resumeToken, fields: step.fields },
|
|
1768
|
+
outputs: { waiting: true },
|
|
1769
|
+
error: null,
|
|
1770
|
+
retryCount: 0,
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
return createStepResult(step.id, StepStatus.COMPLETED, {
|
|
1774
|
+
waiting: true,
|
|
1775
|
+
mode: 'form',
|
|
1776
|
+
resumeToken,
|
|
1777
|
+
fields: step.fields,
|
|
1778
|
+
formPath: `/form/${context.runId}/${step.id}/${resumeToken}`,
|
|
1779
|
+
}, startedAt);
|
|
1780
|
+
}
|
|
1781
|
+
default:
|
|
1782
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, `Unknown wait mode: ${step.mode}`);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
catch (error) {
|
|
1786
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
// ============================================================================
|
|
1790
|
+
// Merge Step Execution
|
|
1791
|
+
// ============================================================================
|
|
1792
|
+
/**
|
|
1793
|
+
* Execute a merge step - combines data from multiple sources.
|
|
1794
|
+
*/
|
|
1795
|
+
async executeMergeStep(step, context) {
|
|
1796
|
+
const startedAt = new Date();
|
|
1797
|
+
try {
|
|
1798
|
+
// Resolve all source expressions to arrays
|
|
1799
|
+
const resolvedSources = [];
|
|
1800
|
+
for (const source of step.sources) {
|
|
1801
|
+
const resolved = resolveTemplates(source, context);
|
|
1802
|
+
if (!Array.isArray(resolved)) {
|
|
1803
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, `Merge source "${source}" did not resolve to an array`);
|
|
1804
|
+
}
|
|
1805
|
+
resolvedSources.push(resolved);
|
|
1806
|
+
}
|
|
1807
|
+
let result;
|
|
1808
|
+
switch (step.mode) {
|
|
1809
|
+
case 'append':
|
|
1810
|
+
result = resolvedSources.flat();
|
|
1811
|
+
break;
|
|
1812
|
+
case 'match': {
|
|
1813
|
+
if (!step.matchField) {
|
|
1814
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Merge mode "match" requires matchField');
|
|
1815
|
+
}
|
|
1816
|
+
// Return items that appear in ALL sources (by matchField)
|
|
1817
|
+
const fieldSets = resolvedSources.map((source) => new Set(source.map((item) => getField(item, step.matchField))));
|
|
1818
|
+
// Intersection of all field sets
|
|
1819
|
+
const commonKeys = fieldSets.reduce((acc, set) => new Set([...acc].filter((key) => set.has(key))));
|
|
1820
|
+
// Return items from first source that match
|
|
1821
|
+
result = resolvedSources[0].filter((item) => commonKeys.has(getField(item, step.matchField)));
|
|
1822
|
+
break;
|
|
1823
|
+
}
|
|
1824
|
+
case 'diff': {
|
|
1825
|
+
if (!step.matchField) {
|
|
1826
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Merge mode "diff" requires matchField');
|
|
1827
|
+
}
|
|
1828
|
+
// Return items from first source NOT found in other sources
|
|
1829
|
+
const otherKeys = new Set(resolvedSources.slice(1).flat().map((item) => getField(item, step.matchField)));
|
|
1830
|
+
result = resolvedSources[0].filter((item) => !otherKeys.has(getField(item, step.matchField)));
|
|
1831
|
+
break;
|
|
1832
|
+
}
|
|
1833
|
+
case 'combine_by_field': {
|
|
1834
|
+
if (!step.matchField) {
|
|
1835
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Merge mode "combine_by_field" requires matchField');
|
|
1836
|
+
}
|
|
1837
|
+
// Group items by matchField and merge their properties
|
|
1838
|
+
const grouped = new Map();
|
|
1839
|
+
const onConflict = step.onConflict ?? 'keep_last';
|
|
1840
|
+
for (const source of resolvedSources) {
|
|
1841
|
+
for (const item of source) {
|
|
1842
|
+
if (!item || typeof item !== 'object')
|
|
1843
|
+
continue;
|
|
1844
|
+
const key = getField(item, step.matchField);
|
|
1845
|
+
const existing = grouped.get(key);
|
|
1846
|
+
if (existing) {
|
|
1847
|
+
if (onConflict === 'keep_first') {
|
|
1848
|
+
// Only add new fields
|
|
1849
|
+
for (const [k, v] of Object.entries(item)) {
|
|
1850
|
+
if (!(k in existing))
|
|
1851
|
+
existing[k] = v;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
else if (onConflict === 'keep_last') {
|
|
1855
|
+
Object.assign(existing, item);
|
|
1856
|
+
}
|
|
1857
|
+
else {
|
|
1858
|
+
// merge_fields: deep merge
|
|
1859
|
+
Object.assign(existing, item);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
else {
|
|
1863
|
+
grouped.set(key, { ...item });
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
result = Array.from(grouped.values());
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
default:
|
|
1871
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, `Unknown merge mode: ${step.mode}`);
|
|
1872
|
+
}
|
|
1873
|
+
return createStepResult(step.id, StepStatus.COMPLETED, result, startedAt);
|
|
1874
|
+
}
|
|
1875
|
+
catch (error) {
|
|
1876
|
+
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
// ============================================================================
|
|
1880
|
+
// Helper Methods for Control Flow
|
|
1881
|
+
// ============================================================================
|
|
1882
|
+
/**
|
|
1883
|
+
* Clone execution context for parallel branches.
|
|
1884
|
+
*/
|
|
1885
|
+
cloneContext(context) {
|
|
1886
|
+
return {
|
|
1887
|
+
...context,
|
|
1888
|
+
variables: { ...context.variables },
|
|
1889
|
+
inputs: { ...context.inputs },
|
|
1890
|
+
stepMetadata: { ...context.stepMetadata },
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Merge branch context back into main context.
|
|
1895
|
+
*/
|
|
1896
|
+
mergeContexts(mainContext, branchContext, branchId) {
|
|
1897
|
+
// Merge variables with branch prefix
|
|
1898
|
+
for (const [key, value] of Object.entries(branchContext.variables)) {
|
|
1899
|
+
mainContext.variables[`${branchId}.${key}`] = value;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Execute promises with concurrency limit.
|
|
1904
|
+
*/
|
|
1905
|
+
async executeConcurrentlyWithLimit(promises, limit) {
|
|
1906
|
+
const results = [];
|
|
1907
|
+
const executing = [];
|
|
1908
|
+
for (const promise of promises) {
|
|
1909
|
+
const p = promise.then((result) => {
|
|
1910
|
+
results.push(result);
|
|
1911
|
+
});
|
|
1912
|
+
executing.push(p);
|
|
1913
|
+
if (executing.length >= limit) {
|
|
1914
|
+
await Promise.race(executing);
|
|
1915
|
+
executing.splice(executing.findIndex((x) => x === p), 1);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
await Promise.all(executing);
|
|
1919
|
+
return results;
|
|
1920
|
+
}
|
|
585
1921
|
}
|
|
586
1922
|
// ============================================================================
|
|
587
1923
|
// Helpers
|