@marktoflow/core 2.0.0-alpha.15 → 2.0.0-alpha.16
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 -365
- package/dist/built-in-operations.d.ts +10 -0
- package/dist/built-in-operations.d.ts.map +1 -1
- package/dist/built-in-operations.js +386 -1
- package/dist/built-in-operations.js.map +1 -1
- 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 +36 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +462 -21
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +399 -6
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +60 -1
- package/dist/models.js.map +1 -1
- 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 +19 -0
- package/dist/parser.js.map +1 -1
- package/dist/sdk-registry.d.ts +7 -1
- package/dist/sdk-registry.d.ts.map +1 -1
- package/dist/sdk-registry.js +38 -9
- 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/secrets/index.d.ts +12 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +11 -0
- package/dist/secrets/index.js.map +1 -0
- package/dist/secrets/providers/aws.d.ts +32 -0
- package/dist/secrets/providers/aws.d.ts.map +1 -0
- package/dist/secrets/providers/aws.js +118 -0
- package/dist/secrets/providers/aws.js.map +1 -0
- package/dist/secrets/providers/azure.d.ts +40 -0
- package/dist/secrets/providers/azure.d.ts.map +1 -0
- package/dist/secrets/providers/azure.js +170 -0
- package/dist/secrets/providers/azure.js.map +1 -0
- package/dist/secrets/providers/env.d.ts +26 -0
- package/dist/secrets/providers/env.d.ts.map +1 -0
- package/dist/secrets/providers/env.js +59 -0
- package/dist/secrets/providers/env.js.map +1 -0
- package/dist/secrets/providers/vault.d.ts +39 -0
- package/dist/secrets/providers/vault.d.ts.map +1 -0
- package/dist/secrets/providers/vault.js +180 -0
- package/dist/secrets/providers/vault.js.map +1 -0
- package/dist/secrets/secret-manager.d.ts +72 -0
- package/dist/secrets/secret-manager.d.ts.map +1 -0
- package/dist/secrets/secret-manager.js +226 -0
- package/dist/secrets/secret-manager.js.map +1 -0
- package/dist/secrets/types.d.ts +105 -0
- package/dist/secrets/types.d.ts.map +1 -0
- package/dist/secrets/types.js +8 -0
- package/dist/secrets/types.js.map +1 -0
- package/package.json +1 -1
- package/dist/expression-helpers.d.ts +0 -309
- package/dist/expression-helpers.d.ts.map +0 -1
- package/dist/expression-helpers.js +0 -697
- package/dist/expression-helpers.js.map +0 -1
- package/dist/pipeline-parser.d.ts +0 -38
- package/dist/pipeline-parser.d.ts.map +0 -1
- package/dist/pipeline-parser.js +0 -219
- package/dist/pipeline-parser.js.map +0 -1
- package/dist/regex-operators.d.ts +0 -86
- package/dist/regex-operators.d.ts.map +0 -1
- package/dist/regex-operators.js +0 -383
- package/dist/regex-operators.js.map +0 -1
- package/dist/version.d.ts +0 -8
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -8
- package/dist/version.js.map +0 -1
package/dist/engine.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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, isScriptStep, } 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
8
|
import { mergePermissions, toSecurityPolicy, } from './permissions.js';
|
|
9
9
|
import { loadPromptFile, resolvePromptTemplate, validatePromptInputs, } from './prompt-loader.js';
|
|
10
10
|
import { DEFAULT_FAILOVER_CONFIG, AgentHealthTracker, FailoverReason, } from './failover.js';
|
|
@@ -16,6 +16,28 @@ import { executeScriptAsync } from './script-executor.js';
|
|
|
16
16
|
// ============================================================================
|
|
17
17
|
// Helper Functions
|
|
18
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
|
+
}
|
|
19
41
|
/**
|
|
20
42
|
* Convert error to string for display/logging
|
|
21
43
|
*/
|
|
@@ -33,6 +55,21 @@ function errorToString(error) {
|
|
|
33
55
|
return String(error);
|
|
34
56
|
}
|
|
35
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
|
+
}
|
|
36
73
|
// ============================================================================
|
|
37
74
|
// Retry Policy
|
|
38
75
|
// ============================================================================
|
|
@@ -247,7 +284,7 @@ export class WorkflowEngine {
|
|
|
247
284
|
failoverConfig;
|
|
248
285
|
healthTracker;
|
|
249
286
|
failoverEvents = [];
|
|
250
|
-
workflowPath; // Base path for resolving sub-workflows
|
|
287
|
+
workflowPath; // Base path for resolving sub-workflows (public for CLI state tracking)
|
|
251
288
|
workflowPermissions; // Workflow-level permissions
|
|
252
289
|
promptCache = new Map(); // Cache for loaded prompts
|
|
253
290
|
constructor(config = {}, events = {}, stateStore) {
|
|
@@ -305,6 +342,12 @@ export class WorkflowEngine {
|
|
|
305
342
|
if (isScriptStep(step)) {
|
|
306
343
|
return this.executeScriptStep(step, context);
|
|
307
344
|
}
|
|
345
|
+
if (isWaitStep(step)) {
|
|
346
|
+
return this.executeWaitStep(step, context);
|
|
347
|
+
}
|
|
348
|
+
if (isMergeStep(step)) {
|
|
349
|
+
return this.executeMergeStep(step, context);
|
|
350
|
+
}
|
|
308
351
|
// Default: action or workflow step
|
|
309
352
|
return this.executeStepWithFailover(step, context, sdkRegistry, stepExecutor);
|
|
310
353
|
}
|
|
@@ -330,7 +373,7 @@ export class WorkflowEngine {
|
|
|
330
373
|
this.stateStore.createExecution({
|
|
331
374
|
runId: context.runId,
|
|
332
375
|
workflowId: workflow.metadata.id,
|
|
333
|
-
workflowPath: 'unknown',
|
|
376
|
+
workflowPath: this.workflowPath ?? 'unknown',
|
|
334
377
|
status: WorkflowStatus.RUNNING,
|
|
335
378
|
startedAt: startedAt,
|
|
336
379
|
completedAt: null,
|
|
@@ -427,6 +470,136 @@ export class WorkflowEngine {
|
|
|
427
470
|
this.events.onWorkflowComplete?.(workflow, workflowResult);
|
|
428
471
|
return workflowResult;
|
|
429
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
|
+
}
|
|
430
603
|
/**
|
|
431
604
|
* Execute a workflow from a file.
|
|
432
605
|
* This method automatically sets the workflow path for resolving sub-workflows.
|
|
@@ -1116,9 +1289,17 @@ Execute the workflow steps in order and return the final outputs as JSON.`;
|
|
|
1116
1289
|
}
|
|
1117
1290
|
/**
|
|
1118
1291
|
* Execute a for-each loop step.
|
|
1292
|
+
* Supports optional batch_size and pause_between_batches for rate limiting.
|
|
1119
1293
|
*/
|
|
1120
1294
|
async executeForEachStep(step, context, sdkRegistry, stepExecutor) {
|
|
1121
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
|
+
};
|
|
1122
1303
|
try {
|
|
1123
1304
|
// Resolve items array
|
|
1124
1305
|
const items = resolveTemplates(step.items, context);
|
|
@@ -1128,10 +1309,15 @@ Execute the workflow steps in order and return the final outputs as JSON.`;
|
|
|
1128
1309
|
if (items.length === 0) {
|
|
1129
1310
|
return createStepResult(step.id, StepStatus.SKIPPED, [], startedAt);
|
|
1130
1311
|
}
|
|
1131
|
-
|
|
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
|
|
1132
1319
|
const results = [];
|
|
1133
1320
|
for (let i = 0; i < items.length; i++) {
|
|
1134
|
-
// Inject loop variables
|
|
1135
1321
|
context.variables[step.itemVariable] = items[i];
|
|
1136
1322
|
context.variables['loop'] = {
|
|
1137
1323
|
index: i,
|
|
@@ -1151,35 +1337,82 @@ Execute the workflow steps in order and return the final outputs as JSON.`;
|
|
|
1151
1337
|
if (result.status === StepStatus.FAILED) {
|
|
1152
1338
|
const errorAction = step.errorHandling?.action ?? 'stop';
|
|
1153
1339
|
if (errorAction === 'stop') {
|
|
1154
|
-
|
|
1155
|
-
delete context.variables[step.itemVariable];
|
|
1156
|
-
delete context.variables['loop'];
|
|
1157
|
-
if (step.indexVariable)
|
|
1158
|
-
delete context.variables[step.indexVariable];
|
|
1340
|
+
cleanupLoopVars();
|
|
1159
1341
|
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
|
|
1160
1342
|
}
|
|
1161
|
-
// 'continue' - skip to next iteration
|
|
1162
1343
|
break;
|
|
1163
1344
|
}
|
|
1164
1345
|
}
|
|
1165
1346
|
results.push(context.variables[step.itemVariable]);
|
|
1166
1347
|
}
|
|
1167
|
-
|
|
1168
|
-
delete context.variables[step.itemVariable];
|
|
1169
|
-
delete context.variables['loop'];
|
|
1170
|
-
if (step.indexVariable)
|
|
1171
|
-
delete context.variables[step.indexVariable];
|
|
1348
|
+
cleanupLoopVars();
|
|
1172
1349
|
return createStepResult(step.id, StepStatus.COMPLETED, results, startedAt);
|
|
1173
1350
|
}
|
|
1174
1351
|
catch (error) {
|
|
1175
|
-
|
|
1176
|
-
delete context.variables[step.itemVariable];
|
|
1177
|
-
delete context.variables['loop'];
|
|
1178
|
-
if (step.indexVariable)
|
|
1179
|
-
delete context.variables[step.indexVariable];
|
|
1352
|
+
cleanupLoopVars();
|
|
1180
1353
|
return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
|
|
1181
1354
|
}
|
|
1182
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
|
+
}
|
|
1183
1416
|
/**
|
|
1184
1417
|
* Execute a while loop step.
|
|
1185
1418
|
*/
|
|
@@ -1436,6 +1669,214 @@ Execute the workflow steps in order and return the final outputs as JSON.`;
|
|
|
1436
1669
|
}
|
|
1437
1670
|
}
|
|
1438
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
|
+
// ============================================================================
|
|
1439
1880
|
// Helper Methods for Control Flow
|
|
1440
1881
|
// ============================================================================
|
|
1441
1882
|
/**
|