@marktoflow/core 2.0.0-alpha.9 → 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 -222
- 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 +92 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +937 -59
- 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 +1441 -54
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +124 -2
- 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 +113 -3
- 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 +30 -12
- package/LICENSE +0 -201
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, isActionStep, isSubWorkflowStep, isIfStep, isSwitchStep, isForEachStep, isWhileStep, isMapStep, isFilterStep, isReduceStep, isParallelStep, isTryStep, } 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,26 +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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Otherwise, do string interpolation
|
|
110
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (_, varPath) => {
|
|
111
|
-
const path = varPath.trim();
|
|
112
|
-
const resolved = resolveVariablePath(path, context);
|
|
113
|
-
return resolved !== undefined ? String(resolved) : '';
|
|
114
|
-
});
|
|
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);
|
|
115
176
|
}
|
|
116
177
|
if (Array.isArray(value)) {
|
|
117
178
|
return value.map((v) => resolveTemplates(v, context));
|
|
@@ -141,6 +202,11 @@ export function resolveVariablePath(path, context) {
|
|
|
141
202
|
if (fromVars !== undefined) {
|
|
142
203
|
return fromVars;
|
|
143
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
|
+
}
|
|
144
210
|
// Check step metadata (for status checks like: step_id.status)
|
|
145
211
|
const fromStepMeta = getNestedValue(context.stepMetadata, path);
|
|
146
212
|
if (fromStepMeta !== undefined) {
|
|
@@ -150,26 +216,63 @@ export function resolveVariablePath(path, context) {
|
|
|
150
216
|
return getNestedValue(context, path);
|
|
151
217
|
}
|
|
152
218
|
/**
|
|
153
|
-
* 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"
|
|
154
221
|
*/
|
|
155
222
|
function getNestedValue(obj, path) {
|
|
156
223
|
if (obj === null || obj === undefined) {
|
|
157
224
|
return undefined;
|
|
158
225
|
}
|
|
159
|
-
|
|
160
|
-
|
|
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;
|
|
161
259
|
for (const part of parts) {
|
|
162
|
-
if (
|
|
260
|
+
if (result === null || result === undefined) {
|
|
163
261
|
return undefined;
|
|
164
262
|
}
|
|
165
|
-
if
|
|
166
|
-
|
|
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];
|
|
167
270
|
}
|
|
168
271
|
else {
|
|
169
272
|
return undefined;
|
|
170
273
|
}
|
|
171
274
|
}
|
|
172
|
-
return
|
|
275
|
+
return result;
|
|
173
276
|
}
|
|
174
277
|
export class WorkflowEngine {
|
|
175
278
|
config;
|
|
@@ -181,13 +284,17 @@ export class WorkflowEngine {
|
|
|
181
284
|
failoverConfig;
|
|
182
285
|
healthTracker;
|
|
183
286
|
failoverEvents = [];
|
|
184
|
-
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
|
|
185
290
|
constructor(config = {}, events = {}, stateStore) {
|
|
186
291
|
this.config = {
|
|
187
292
|
defaultTimeout: config.defaultTimeout ?? 60000,
|
|
188
293
|
maxRetries: config.maxRetries ?? 3,
|
|
189
294
|
retryBaseDelay: config.retryBaseDelay ?? 1000,
|
|
190
295
|
retryMaxDelay: config.retryMaxDelay ?? 30000,
|
|
296
|
+
defaultAgent: config.defaultAgent,
|
|
297
|
+
defaultModel: config.defaultModel,
|
|
191
298
|
};
|
|
192
299
|
this.retryPolicy = new RetryPolicy(this.config.maxRetries, this.config.retryBaseDelay, this.config.retryMaxDelay);
|
|
193
300
|
this.events = events;
|
|
@@ -232,6 +339,15 @@ export class WorkflowEngine {
|
|
|
232
339
|
if (isTryStep(step)) {
|
|
233
340
|
return this.executeTryStep(step, context, sdkRegistry, stepExecutor);
|
|
234
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
|
+
}
|
|
235
351
|
// Default: action or workflow step
|
|
236
352
|
return this.executeStepWithFailover(step, context, sdkRegistry, stepExecutor);
|
|
237
353
|
}
|
|
@@ -242,13 +358,22 @@ export class WorkflowEngine {
|
|
|
242
358
|
const context = createExecutionContext(workflow, inputs);
|
|
243
359
|
const stepResults = [];
|
|
244
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
|
+
}
|
|
245
370
|
context.status = WorkflowStatus.RUNNING;
|
|
246
371
|
this.events.onWorkflowStart?.(workflow, context);
|
|
247
372
|
if (this.stateStore) {
|
|
248
373
|
this.stateStore.createExecution({
|
|
249
374
|
runId: context.runId,
|
|
250
375
|
workflowId: workflow.metadata.id,
|
|
251
|
-
workflowPath: 'unknown',
|
|
376
|
+
workflowPath: this.workflowPath ?? 'unknown',
|
|
252
377
|
status: WorkflowStatus.RUNNING,
|
|
253
378
|
startedAt: startedAt,
|
|
254
379
|
completedAt: null,
|
|
@@ -272,12 +397,21 @@ export class WorkflowEngine {
|
|
|
272
397
|
context.stepMetadata[step.id] = {
|
|
273
398
|
status: result.status.toLowerCase(),
|
|
274
399
|
retryCount: result.retryCount,
|
|
275
|
-
...(result.error ? { error: result.error } : {}),
|
|
400
|
+
...(result.error ? { error: errorToString(result.error) } : {}),
|
|
276
401
|
};
|
|
277
402
|
// Store output variable
|
|
278
403
|
if (step.outputVariable && result.status === StepStatus.COMPLETED) {
|
|
279
404
|
context.variables[step.outputVariable] = result.output;
|
|
280
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
|
+
}
|
|
281
415
|
// Handle failure
|
|
282
416
|
if (result.status === StepStatus.FAILED) {
|
|
283
417
|
// Get error action from step if it has error handling
|
|
@@ -287,7 +421,7 @@ export class WorkflowEngine {
|
|
|
287
421
|
}
|
|
288
422
|
if (errorAction === 'stop') {
|
|
289
423
|
context.status = WorkflowStatus.FAILED;
|
|
290
|
-
const workflowError = result.error
|
|
424
|
+
const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
|
|
291
425
|
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
|
|
292
426
|
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
293
427
|
return workflowResult;
|
|
@@ -302,7 +436,7 @@ export class WorkflowEngine {
|
|
|
302
436
|
});
|
|
303
437
|
}
|
|
304
438
|
context.status = WorkflowStatus.FAILED;
|
|
305
|
-
const workflowError = result.error
|
|
439
|
+
const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
|
|
306
440
|
const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
|
|
307
441
|
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
308
442
|
return workflowResult;
|
|
@@ -336,6 +470,136 @@ export class WorkflowEngine {
|
|
|
336
470
|
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
337
471
|
return workflowResult;
|
|
338
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
|
+
}
|
|
339
603
|
/**
|
|
340
604
|
* Execute a workflow from a file.
|
|
341
605
|
* This method automatically sets the workflow path for resolving sub-workflows.
|
|
@@ -390,14 +654,302 @@ export class WorkflowEngine {
|
|
|
390
654
|
// Return the sub-workflow output
|
|
391
655
|
return result.output;
|
|
392
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
|
+
}
|
|
393
926
|
/**
|
|
394
927
|
* Execute a step with retry logic.
|
|
395
928
|
*/
|
|
396
929
|
async executeStepWithRetry(step, context, sdkRegistry, stepExecutor) {
|
|
397
930
|
const startedAt = new Date();
|
|
398
931
|
let lastError;
|
|
932
|
+
// Build executor context with model/agent/permissions
|
|
933
|
+
const executorContext = this.buildStepExecutorContext(step);
|
|
399
934
|
// Handle sub-workflow execution
|
|
400
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
|
|
401
953
|
try {
|
|
402
954
|
this.events.onStepStart?.(step, context);
|
|
403
955
|
const output = await this.executeWithTimeout(() => this.executeSubWorkflow(step, context, sdkRegistry, stepExecutor), step.timeout ?? this.config.defaultTimeout);
|
|
@@ -407,7 +959,8 @@ export class WorkflowEngine {
|
|
|
407
959
|
}
|
|
408
960
|
catch (error) {
|
|
409
961
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
410
|
-
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
|
+
);
|
|
411
964
|
this.events.onStepComplete?.(step, result);
|
|
412
965
|
return result;
|
|
413
966
|
}
|
|
@@ -431,11 +984,30 @@ export class WorkflowEngine {
|
|
|
431
984
|
}
|
|
432
985
|
this.events.onStepStart?.(step, context);
|
|
433
986
|
try {
|
|
434
|
-
//
|
|
435
|
-
|
|
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
|
+
}
|
|
436
997
|
const stepWithResolvedInputs = { ...step, inputs: resolvedInputs };
|
|
437
|
-
//
|
|
438
|
-
|
|
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
|
+
}
|
|
439
1011
|
circuitBreaker.recordSuccess();
|
|
440
1012
|
const result = createStepResult(step.id, StepStatus.COMPLETED, output, startedAt, attempt);
|
|
441
1013
|
this.events.onStepComplete?.(step, result);
|
|
@@ -453,7 +1025,8 @@ export class WorkflowEngine {
|
|
|
453
1025
|
}
|
|
454
1026
|
}
|
|
455
1027
|
// All retries exhausted
|
|
456
|
-
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
|
+
);
|
|
457
1030
|
this.events.onStepComplete?.(step, result);
|
|
458
1031
|
return result;
|
|
459
1032
|
}
|
|
@@ -472,7 +1045,7 @@ export class WorkflowEngine {
|
|
|
472
1045
|
this.healthTracker.markHealthy(primaryTool);
|
|
473
1046
|
return primaryResult;
|
|
474
1047
|
}
|
|
475
|
-
const errorMessage = primaryResult.error
|
|
1048
|
+
const errorMessage = primaryResult.error ? errorToString(primaryResult.error) : '';
|
|
476
1049
|
const isTimeout = errorMessage.includes('timed out');
|
|
477
1050
|
if (isTimeout && !this.failoverConfig.failoverOnTimeout) {
|
|
478
1051
|
this.healthTracker.markUnhealthy(primaryTool, errorMessage);
|
|
@@ -576,8 +1149,18 @@ export class WorkflowEngine {
|
|
|
576
1149
|
/**
|
|
577
1150
|
* Resolve a condition value with support for nested properties.
|
|
578
1151
|
* Handles direct variable references and nested paths.
|
|
1152
|
+
* Uses Nunjucks for template expressions with filters/regex.
|
|
579
1153
|
*/
|
|
580
1154
|
resolveConditionValue(path, context) {
|
|
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
|
+
}
|
|
581
1164
|
// First try to parse as a literal value (true, false, numbers, etc.)
|
|
582
1165
|
const parsedValue = this.parseValue(path);
|
|
583
1166
|
// If parseValue returned the same string, try to resolve as a variable
|
|
@@ -615,12 +1198,14 @@ export class WorkflowEngine {
|
|
|
615
1198
|
*/
|
|
616
1199
|
buildWorkflowResult(workflow, context, stepResults, startedAt, error) {
|
|
617
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;
|
|
618
1203
|
return {
|
|
619
1204
|
workflowId: workflow.metadata.id,
|
|
620
1205
|
runId: context.runId,
|
|
621
1206
|
status: context.status,
|
|
622
1207
|
stepResults,
|
|
623
|
-
output
|
|
1208
|
+
output,
|
|
624
1209
|
error,
|
|
625
1210
|
startedAt,
|
|
626
1211
|
completedAt,
|
|
@@ -704,9 +1289,17 @@ export class WorkflowEngine {
|
|
|
704
1289
|
}
|
|
705
1290
|
/**
|
|
706
1291
|
* Execute a for-each loop step.
|
|
1292
|
+
* Supports optional batch_size and pause_between_batches for rate limiting.
|
|
707
1293
|
*/
|
|
708
1294
|
async executeForEachStep(step, context, sdkRegistry, stepExecutor) {
|
|
709
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
|
+
};
|
|
710
1303
|
try {
|
|
711
1304
|
// Resolve items array
|
|
712
1305
|
const items = resolveTemplates(step.items, context);
|
|
@@ -716,10 +1309,15 @@ export class WorkflowEngine {
|
|
|
716
1309
|
if (items.length === 0) {
|
|
717
1310
|
return createStepResult(step.id, StepStatus.SKIPPED, [], startedAt);
|
|
718
1311
|
}
|
|
719
|
-
|
|
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
|
|
720
1319
|
const results = [];
|
|
721
1320
|
for (let i = 0; i < items.length; i++) {
|
|
722
|
-
// Inject loop variables
|
|
723
1321
|
context.variables[step.itemVariable] = items[i];
|
|
724
1322
|
context.variables['loop'] = {
|
|
725
1323
|
index: i,
|
|
@@ -739,35 +1337,82 @@ export class WorkflowEngine {
|
|
|
739
1337
|
if (result.status === StepStatus.FAILED) {
|
|
740
1338
|
const errorAction = step.errorHandling?.action ?? 'stop';
|
|
741
1339
|
if (errorAction === 'stop') {
|
|
742
|
-
|
|
743
|
-
delete context.variables[step.itemVariable];
|
|
744
|
-
delete context.variables['loop'];
|
|
745
|
-
if (step.indexVariable)
|
|
746
|
-
delete context.variables[step.indexVariable];
|
|
1340
|
+
cleanupLoopVars();
|
|
747
1341
|
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
748
1342
|
}
|
|
749
|
-
// 'continue' - skip to next iteration
|
|
750
1343
|
break;
|
|
751
1344
|
}
|
|
752
1345
|
}
|
|
753
1346
|
results.push(context.variables[step.itemVariable]);
|
|
754
1347
|
}
|
|
755
|
-
|
|
756
|
-
delete context.variables[step.itemVariable];
|
|
757
|
-
delete context.variables['loop'];
|
|
758
|
-
if (step.indexVariable)
|
|
759
|
-
delete context.variables[step.indexVariable];
|
|
1348
|
+
cleanupLoopVars();
|
|
760
1349
|
return createStepResult(step.id, StepStatus.COMPLETED, results, startedAt);
|
|
761
1350
|
}
|
|
762
1351
|
catch (error) {
|
|
763
|
-
|
|
764
|
-
delete context.variables[step.itemVariable];
|
|
765
|
-
delete context.variables['loop'];
|
|
766
|
-
if (step.indexVariable)
|
|
767
|
-
delete context.variables[step.indexVariable];
|
|
1352
|
+
cleanupLoopVars();
|
|
768
1353
|
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
769
1354
|
}
|
|
770
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
|
+
}
|
|
771
1416
|
/**
|
|
772
1417
|
* Execute a while loop step.
|
|
773
1418
|
*/
|
|
@@ -899,7 +1544,7 @@ export class WorkflowEngine {
|
|
|
899
1544
|
branchResults.push(result.output);
|
|
900
1545
|
}
|
|
901
1546
|
if (result.status === StepStatus.FAILED) {
|
|
902
|
-
throw new Error(`Branch ${branch.id} failed: ${result.error}`);
|
|
1547
|
+
throw new Error(`Branch ${branch.id} failed: ${errorToString(result.error)}`);
|
|
903
1548
|
}
|
|
904
1549
|
}
|
|
905
1550
|
return { branchId: branch.id, context: branchContext, results: branchResults };
|
|
@@ -936,7 +1581,7 @@ export class WorkflowEngine {
|
|
|
936
1581
|
context.variables[tryStep.outputVariable] = result.output;
|
|
937
1582
|
}
|
|
938
1583
|
if (result.status === StepStatus.FAILED) {
|
|
939
|
-
tryError = new Error(result.error
|
|
1584
|
+
tryError = new Error(result.error ? errorToString(result.error) : 'Step failed');
|
|
940
1585
|
break;
|
|
941
1586
|
}
|
|
942
1587
|
}
|
|
@@ -954,7 +1599,7 @@ export class WorkflowEngine {
|
|
|
954
1599
|
context.variables[catchStep.outputVariable] = result.output;
|
|
955
1600
|
}
|
|
956
1601
|
if (result.status === StepStatus.FAILED) {
|
|
957
|
-
catchError = new Error(result.error
|
|
1602
|
+
catchError = new Error(result.error ? errorToString(result.error) : 'Catch block failed');
|
|
958
1603
|
break;
|
|
959
1604
|
}
|
|
960
1605
|
}
|
|
@@ -998,6 +1643,239 @@ export class WorkflowEngine {
|
|
|
998
1643
|
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
999
1644
|
}
|
|
1000
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
|
+
}
|
|
1001
1879
|
// ============================================================================
|
|
1002
1880
|
// Helper Methods for Control Flow
|
|
1003
1881
|
// ============================================================================
|