musubi-sdd 3.10.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -19
- package/package.json +1 -1
- package/src/agents/agent-loop.js +532 -0
- package/src/agents/agentic/code-generator.js +767 -0
- package/src/agents/agentic/code-reviewer.js +698 -0
- package/src/agents/agentic/index.js +43 -0
- package/src/agents/function-tool.js +432 -0
- package/src/agents/index.js +45 -0
- package/src/agents/schema-generator.js +514 -0
- package/src/analyzers/ast-extractor.js +870 -0
- package/src/analyzers/context-optimizer.js +681 -0
- package/src/analyzers/repository-map.js +692 -0
- package/src/integrations/index.js +7 -1
- package/src/integrations/mcp/index.js +175 -0
- package/src/integrations/mcp/mcp-context-provider.js +472 -0
- package/src/integrations/mcp/mcp-discovery.js +436 -0
- package/src/integrations/mcp/mcp-tool-registry.js +467 -0
- package/src/integrations/mcp-connector.js +818 -0
- package/src/integrations/tool-discovery.js +589 -0
- package/src/managers/index.js +7 -0
- package/src/managers/skill-tools.js +565 -0
- package/src/monitoring/cost-tracker.js +7 -0
- package/src/monitoring/incident-manager.js +10 -0
- package/src/monitoring/observability.js +10 -0
- package/src/monitoring/quality-dashboard.js +491 -0
- package/src/monitoring/release-manager.js +10 -0
- package/src/orchestration/agent-skill-binding.js +655 -0
- package/src/orchestration/error-handler.js +827 -0
- package/src/orchestration/index.js +235 -1
- package/src/orchestration/mcp-tool-adapters.js +896 -0
- package/src/orchestration/reasoning/index.js +58 -0
- package/src/orchestration/reasoning/planning-engine.js +831 -0
- package/src/orchestration/reasoning/reasoning-engine.js +710 -0
- package/src/orchestration/reasoning/self-correction.js +751 -0
- package/src/orchestration/skill-executor.js +665 -0
- package/src/orchestration/skill-registry.js +650 -0
- package/src/orchestration/workflow-examples.js +1072 -0
- package/src/orchestration/workflow-executor.js +779 -0
- package/src/phase4-integration.js +248 -0
- package/src/phase5-integration.js +402 -0
- package/src/steering/steering-auto-update.js +572 -0
- package/src/steering/steering-validator.js +547 -0
- package/src/templates/template-constraints.js +646 -0
- package/src/validators/advanced-validation.js +580 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowExecutor - End-to-end workflow execution engine
|
|
3
|
+
* Sprint 3.5: Advanced Workflows
|
|
4
|
+
*
|
|
5
|
+
* Provides comprehensive workflow execution with:
|
|
6
|
+
* - Step-by-step execution with state management
|
|
7
|
+
* - Parallel and sequential step execution
|
|
8
|
+
* - Conditional branching and loops
|
|
9
|
+
* - Error handling and recovery
|
|
10
|
+
* - Progress tracking and reporting
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const EventEmitter = require('events');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Workflow step types
|
|
17
|
+
*/
|
|
18
|
+
const StepType = {
|
|
19
|
+
SKILL: 'skill',
|
|
20
|
+
TOOL: 'tool',
|
|
21
|
+
CONDITION: 'condition',
|
|
22
|
+
PARALLEL: 'parallel',
|
|
23
|
+
LOOP: 'loop',
|
|
24
|
+
CHECKPOINT: 'checkpoint',
|
|
25
|
+
HUMAN_REVIEW: 'human-review'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Execution states
|
|
30
|
+
*/
|
|
31
|
+
const ExecutionState = {
|
|
32
|
+
PENDING: 'pending',
|
|
33
|
+
RUNNING: 'running',
|
|
34
|
+
PAUSED: 'paused',
|
|
35
|
+
COMPLETED: 'completed',
|
|
36
|
+
FAILED: 'failed',
|
|
37
|
+
CANCELLED: 'cancelled',
|
|
38
|
+
WAITING_REVIEW: 'waiting-review'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Error recovery strategies
|
|
43
|
+
*/
|
|
44
|
+
const RecoveryStrategy = {
|
|
45
|
+
RETRY: 'retry',
|
|
46
|
+
SKIP: 'skip',
|
|
47
|
+
FALLBACK: 'fallback',
|
|
48
|
+
ROLLBACK: 'rollback',
|
|
49
|
+
ABORT: 'abort',
|
|
50
|
+
MANUAL: 'manual'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Step execution result
|
|
55
|
+
*/
|
|
56
|
+
class StepResult {
|
|
57
|
+
constructor(stepId, success, output = null, error = null, duration = 0) {
|
|
58
|
+
this.stepId = stepId;
|
|
59
|
+
this.success = success;
|
|
60
|
+
this.output = output;
|
|
61
|
+
this.error = error;
|
|
62
|
+
this.duration = duration;
|
|
63
|
+
this.timestamp = new Date().toISOString();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Workflow execution context
|
|
69
|
+
*/
|
|
70
|
+
class ExecutionContext {
|
|
71
|
+
constructor(workflowId) {
|
|
72
|
+
this.workflowId = workflowId;
|
|
73
|
+
this.executionId = `exec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
74
|
+
this.state = ExecutionState.PENDING;
|
|
75
|
+
this.variables = new Map();
|
|
76
|
+
this.stepResults = new Map();
|
|
77
|
+
this.currentStep = null;
|
|
78
|
+
this.startTime = null;
|
|
79
|
+
this.endTime = null;
|
|
80
|
+
this.checkpoints = [];
|
|
81
|
+
this.errors = [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setVariable(name, value) {
|
|
85
|
+
this.variables.set(name, value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getVariable(name, defaultValue = null) {
|
|
89
|
+
return this.variables.has(name) ? this.variables.get(name) : defaultValue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
addStepResult(stepId, result) {
|
|
93
|
+
this.stepResults.set(stepId, result);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
createCheckpoint(name) {
|
|
97
|
+
this.checkpoints.push({
|
|
98
|
+
name,
|
|
99
|
+
timestamp: new Date().toISOString(),
|
|
100
|
+
variables: new Map(this.variables),
|
|
101
|
+
currentStep: this.currentStep
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
restoreCheckpoint(name) {
|
|
106
|
+
const checkpoint = this.checkpoints.find(cp => cp.name === name);
|
|
107
|
+
if (checkpoint) {
|
|
108
|
+
this.variables = new Map(checkpoint.variables);
|
|
109
|
+
this.currentStep = checkpoint.currentStep;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getDuration() {
|
|
116
|
+
if (!this.startTime) return 0;
|
|
117
|
+
const end = this.endTime || Date.now();
|
|
118
|
+
return end - this.startTime;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Workflow definition
|
|
124
|
+
*/
|
|
125
|
+
class WorkflowDefinition {
|
|
126
|
+
constructor(id, name, steps = [], options = {}) {
|
|
127
|
+
this.id = id;
|
|
128
|
+
this.name = name;
|
|
129
|
+
this.description = options.description || '';
|
|
130
|
+
this.version = options.version || '1.0.0';
|
|
131
|
+
this.steps = steps;
|
|
132
|
+
this.inputs = options.inputs || [];
|
|
133
|
+
this.outputs = options.outputs || [];
|
|
134
|
+
this.errorHandling = options.errorHandling || { strategy: RecoveryStrategy.ABORT };
|
|
135
|
+
this.timeout = options.timeout || 0; // 0 = no timeout
|
|
136
|
+
this.retryPolicy = options.retryPolicy || { maxRetries: 3, backoffMs: 1000 };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
validate() {
|
|
140
|
+
const errors = [];
|
|
141
|
+
|
|
142
|
+
if (!this.id) {
|
|
143
|
+
errors.push('Workflow ID is required');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!this.name) {
|
|
147
|
+
errors.push('Workflow name is required');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!this.steps || this.steps.length === 0) {
|
|
151
|
+
errors.push('Workflow must have at least one step');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validate each step
|
|
155
|
+
const stepIds = new Set();
|
|
156
|
+
for (const step of this.steps) {
|
|
157
|
+
if (!step.id) {
|
|
158
|
+
errors.push('Each step must have an ID');
|
|
159
|
+
} else if (stepIds.has(step.id)) {
|
|
160
|
+
errors.push(`Duplicate step ID: ${step.id}`);
|
|
161
|
+
} else {
|
|
162
|
+
stepIds.add(step.id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!step.type) {
|
|
166
|
+
errors.push(`Step ${step.id} must have a type`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
valid: errors.length === 0,
|
|
172
|
+
errors
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Main workflow executor
|
|
179
|
+
*/
|
|
180
|
+
class WorkflowExecutor extends EventEmitter {
|
|
181
|
+
constructor(options = {}) {
|
|
182
|
+
super();
|
|
183
|
+
this.skillRegistry = options.skillRegistry || null;
|
|
184
|
+
this.toolDiscovery = options.toolDiscovery || null;
|
|
185
|
+
this.mcpConnector = options.mcpConnector || null;
|
|
186
|
+
this.executions = new Map();
|
|
187
|
+
this.stepHandlers = new Map();
|
|
188
|
+
|
|
189
|
+
// Register default step handlers
|
|
190
|
+
this._registerDefaultHandlers();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Register default step type handlers
|
|
195
|
+
*/
|
|
196
|
+
_registerDefaultHandlers() {
|
|
197
|
+
// Skill execution handler
|
|
198
|
+
this.stepHandlers.set(StepType.SKILL, async (step, context) => {
|
|
199
|
+
const { skillId, input } = step;
|
|
200
|
+
|
|
201
|
+
if (!this.skillRegistry) {
|
|
202
|
+
throw new Error('Skill registry not configured');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const skill = this.skillRegistry.getSkill(skillId);
|
|
206
|
+
if (!skill) {
|
|
207
|
+
throw new Error(`Skill not found: ${skillId}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Resolve input variables
|
|
211
|
+
const resolvedInput = this._resolveVariables(input, context);
|
|
212
|
+
|
|
213
|
+
// Execute skill
|
|
214
|
+
const result = await skill.execute(resolvedInput, context);
|
|
215
|
+
|
|
216
|
+
// Store output in context
|
|
217
|
+
if (step.outputVariable) {
|
|
218
|
+
context.setVariable(step.outputVariable, result);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Tool execution handler
|
|
225
|
+
this.stepHandlers.set(StepType.TOOL, async (step, context) => {
|
|
226
|
+
const { toolName, serverName, arguments: args } = step;
|
|
227
|
+
|
|
228
|
+
if (!this.mcpConnector) {
|
|
229
|
+
throw new Error('MCP connector not configured');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Resolve arguments
|
|
233
|
+
const resolvedArgs = this._resolveVariables(args, context);
|
|
234
|
+
|
|
235
|
+
// Call tool
|
|
236
|
+
const result = await this.mcpConnector.callTool(toolName, resolvedArgs, serverName);
|
|
237
|
+
|
|
238
|
+
if (step.outputVariable) {
|
|
239
|
+
context.setVariable(step.outputVariable, result);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return result;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Condition handler
|
|
246
|
+
this.stepHandlers.set(StepType.CONDITION, async (step, context) => {
|
|
247
|
+
const { condition, thenSteps, elseSteps } = step;
|
|
248
|
+
|
|
249
|
+
// Evaluate condition
|
|
250
|
+
const conditionResult = this._evaluateCondition(condition, context);
|
|
251
|
+
|
|
252
|
+
// Execute appropriate branch
|
|
253
|
+
const stepsToExecute = conditionResult ? thenSteps : elseSteps;
|
|
254
|
+
|
|
255
|
+
if (stepsToExecute && stepsToExecute.length > 0) {
|
|
256
|
+
for (const subStep of stepsToExecute) {
|
|
257
|
+
await this._executeStep(subStep, context);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return conditionResult;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Parallel execution handler
|
|
265
|
+
this.stepHandlers.set(StepType.PARALLEL, async (step, context) => {
|
|
266
|
+
const { steps, maxConcurrency = 5 } = step;
|
|
267
|
+
|
|
268
|
+
const results = [];
|
|
269
|
+
const executing = new Set();
|
|
270
|
+
|
|
271
|
+
for (const subStep of steps) {
|
|
272
|
+
if (executing.size >= maxConcurrency) {
|
|
273
|
+
const completed = await Promise.race([...executing]);
|
|
274
|
+
executing.delete(completed.promise);
|
|
275
|
+
results.push(completed.result);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const promise = this._executeStep(subStep, context)
|
|
279
|
+
.then(result => ({ promise, result }));
|
|
280
|
+
executing.add(promise);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Wait for remaining
|
|
284
|
+
const remaining = await Promise.all([...executing]);
|
|
285
|
+
results.push(...remaining.map(r => r.result));
|
|
286
|
+
|
|
287
|
+
if (step.outputVariable) {
|
|
288
|
+
context.setVariable(step.outputVariable, results);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return results;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Loop handler
|
|
295
|
+
this.stepHandlers.set(StepType.LOOP, async (step, context) => {
|
|
296
|
+
const { items, itemVariable, indexVariable, steps: loopSteps, maxIterations = 1000 } = step;
|
|
297
|
+
|
|
298
|
+
// Resolve items
|
|
299
|
+
const resolvedItems = this._resolveVariables(items, context);
|
|
300
|
+
|
|
301
|
+
if (!Array.isArray(resolvedItems)) {
|
|
302
|
+
throw new Error('Loop items must be an array');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const results = [];
|
|
306
|
+
let iteration = 0;
|
|
307
|
+
|
|
308
|
+
for (const item of resolvedItems) {
|
|
309
|
+
if (iteration >= maxIterations) {
|
|
310
|
+
this.emit('warning', { message: `Loop reached max iterations: ${maxIterations}` });
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
context.setVariable(itemVariable || 'item', item);
|
|
315
|
+
context.setVariable(indexVariable || 'index', iteration);
|
|
316
|
+
|
|
317
|
+
for (const subStep of loopSteps) {
|
|
318
|
+
const result = await this._executeStep(subStep, context);
|
|
319
|
+
results.push(result);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
iteration++;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (step.outputVariable) {
|
|
326
|
+
context.setVariable(step.outputVariable, results);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return results;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Checkpoint handler
|
|
333
|
+
this.stepHandlers.set(StepType.CHECKPOINT, async (step, context) => {
|
|
334
|
+
const { name } = step;
|
|
335
|
+
context.createCheckpoint(name);
|
|
336
|
+
this.emit('checkpoint', { name, context });
|
|
337
|
+
return { checkpointCreated: name };
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Human review handler
|
|
341
|
+
this.stepHandlers.set(StepType.HUMAN_REVIEW, async (step, context) => {
|
|
342
|
+
const { message, options = ['approve', 'reject'] } = step;
|
|
343
|
+
|
|
344
|
+
context.state = ExecutionState.WAITING_REVIEW;
|
|
345
|
+
this.emit('review-required', {
|
|
346
|
+
stepId: step.id,
|
|
347
|
+
message: this._resolveVariables(message, context),
|
|
348
|
+
options,
|
|
349
|
+
context
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Wait for review (in real implementation, this would wait for external input)
|
|
353
|
+
return { reviewRequested: true, message };
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Register a custom step handler
|
|
359
|
+
*/
|
|
360
|
+
registerStepHandler(type, handler) {
|
|
361
|
+
this.stepHandlers.set(type, handler);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Execute a workflow
|
|
366
|
+
*/
|
|
367
|
+
async execute(workflow, initialVariables = {}) {
|
|
368
|
+
// Validate workflow
|
|
369
|
+
const validation = workflow.validate();
|
|
370
|
+
if (!validation.valid) {
|
|
371
|
+
throw new Error(`Invalid workflow: ${validation.errors.join(', ')}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Create execution context
|
|
375
|
+
const context = new ExecutionContext(workflow.id);
|
|
376
|
+
context.state = ExecutionState.RUNNING;
|
|
377
|
+
context.startTime = Date.now();
|
|
378
|
+
|
|
379
|
+
// Set initial variables
|
|
380
|
+
for (const [key, value] of Object.entries(initialVariables)) {
|
|
381
|
+
context.setVariable(key, value);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Store execution
|
|
385
|
+
this.executions.set(context.executionId, context);
|
|
386
|
+
|
|
387
|
+
this.emit('execution-started', {
|
|
388
|
+
executionId: context.executionId,
|
|
389
|
+
workflowId: workflow.id
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
// Execute steps
|
|
394
|
+
for (const step of workflow.steps) {
|
|
395
|
+
if (context.state === ExecutionState.CANCELLED) {
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (context.state === ExecutionState.PAUSED) {
|
|
400
|
+
await this._waitForResume(context);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await this._executeStep(step, context, workflow);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
context.state = ExecutionState.COMPLETED;
|
|
407
|
+
context.endTime = Date.now();
|
|
408
|
+
|
|
409
|
+
this.emit('execution-completed', {
|
|
410
|
+
executionId: context.executionId,
|
|
411
|
+
duration: context.getDuration(),
|
|
412
|
+
results: Object.fromEntries(context.stepResults)
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
} catch (error) {
|
|
416
|
+
context.state = ExecutionState.FAILED;
|
|
417
|
+
context.endTime = Date.now();
|
|
418
|
+
context.errors.push(error);
|
|
419
|
+
|
|
420
|
+
this.emit('execution-failed', {
|
|
421
|
+
executionId: context.executionId,
|
|
422
|
+
error: error.message,
|
|
423
|
+
step: context.currentStep
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Apply error handling strategy
|
|
427
|
+
await this._handleExecutionError(error, context, workflow);
|
|
428
|
+
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
executionId: context.executionId,
|
|
434
|
+
state: context.state,
|
|
435
|
+
duration: context.getDuration(),
|
|
436
|
+
outputs: this._collectOutputs(context, workflow.outputs),
|
|
437
|
+
stepResults: Object.fromEntries(context.stepResults)
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Execute a single step
|
|
443
|
+
*/
|
|
444
|
+
async _executeStep(step, context, workflow = null) {
|
|
445
|
+
context.currentStep = step.id;
|
|
446
|
+
const startTime = Date.now();
|
|
447
|
+
|
|
448
|
+
this.emit('step-started', { stepId: step.id, type: step.type });
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
// Check condition if present
|
|
452
|
+
if (step.when) {
|
|
453
|
+
const shouldExecute = this._evaluateCondition(step.when, context);
|
|
454
|
+
if (!shouldExecute) {
|
|
455
|
+
this.emit('step-skipped', { stepId: step.id, reason: 'Condition not met' });
|
|
456
|
+
return { skipped: true };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Get handler for step type
|
|
461
|
+
const handler = this.stepHandlers.get(step.type);
|
|
462
|
+
if (!handler) {
|
|
463
|
+
throw new Error(`Unknown step type: ${step.type}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Execute with retry logic
|
|
467
|
+
const result = await this._executeWithRetry(
|
|
468
|
+
() => handler(step, context),
|
|
469
|
+
step.retry || (workflow?.retryPolicy),
|
|
470
|
+
step.id
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const duration = Date.now() - startTime;
|
|
474
|
+
const stepResult = new StepResult(step.id, true, result, null, duration);
|
|
475
|
+
context.addStepResult(step.id, stepResult);
|
|
476
|
+
|
|
477
|
+
this.emit('step-completed', { stepId: step.id, duration, result });
|
|
478
|
+
|
|
479
|
+
return result;
|
|
480
|
+
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const duration = Date.now() - startTime;
|
|
483
|
+
const stepResult = new StepResult(step.id, false, null, error.message, duration);
|
|
484
|
+
context.addStepResult(step.id, stepResult);
|
|
485
|
+
|
|
486
|
+
this.emit('step-failed', { stepId: step.id, error: error.message, duration });
|
|
487
|
+
|
|
488
|
+
// Apply step-level error handling
|
|
489
|
+
if (step.onError) {
|
|
490
|
+
return await this._handleStepError(error, step, context);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Execute with retry logic
|
|
499
|
+
*/
|
|
500
|
+
async _executeWithRetry(fn, retryPolicy, stepId) {
|
|
501
|
+
const maxRetries = retryPolicy?.maxRetries || 0;
|
|
502
|
+
const backoffMs = retryPolicy?.backoffMs || 1000;
|
|
503
|
+
const backoffMultiplier = retryPolicy?.backoffMultiplier || 2;
|
|
504
|
+
|
|
505
|
+
let lastError;
|
|
506
|
+
let currentBackoff = backoffMs;
|
|
507
|
+
|
|
508
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
509
|
+
try {
|
|
510
|
+
return await fn();
|
|
511
|
+
} catch (error) {
|
|
512
|
+
lastError = error;
|
|
513
|
+
|
|
514
|
+
if (attempt < maxRetries) {
|
|
515
|
+
this.emit('step-retry', {
|
|
516
|
+
stepId,
|
|
517
|
+
attempt: attempt + 1,
|
|
518
|
+
maxRetries,
|
|
519
|
+
nextRetryMs: currentBackoff
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await this._sleep(currentBackoff);
|
|
523
|
+
currentBackoff *= backoffMultiplier;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
throw lastError;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Handle step-level error
|
|
533
|
+
*/
|
|
534
|
+
async _handleStepError(error, step, context) {
|
|
535
|
+
const errorConfig = step.onError;
|
|
536
|
+
const strategy = errorConfig.strategy || RecoveryStrategy.ABORT;
|
|
537
|
+
|
|
538
|
+
switch (strategy) {
|
|
539
|
+
case RecoveryStrategy.SKIP:
|
|
540
|
+
this.emit('step-error-skipped', { stepId: step.id, error: error.message });
|
|
541
|
+
return { skipped: true, error: error.message };
|
|
542
|
+
|
|
543
|
+
case RecoveryStrategy.FALLBACK:
|
|
544
|
+
if (errorConfig.fallbackSteps) {
|
|
545
|
+
for (const fallbackStep of errorConfig.fallbackSteps) {
|
|
546
|
+
await this._executeStep(fallbackStep, context);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return { fallback: true };
|
|
550
|
+
|
|
551
|
+
case RecoveryStrategy.ROLLBACK:
|
|
552
|
+
if (errorConfig.rollbackTo) {
|
|
553
|
+
const restored = context.restoreCheckpoint(errorConfig.rollbackTo);
|
|
554
|
+
if (restored) {
|
|
555
|
+
this.emit('rollback', { stepId: step.id, checkpoint: errorConfig.rollbackTo });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
throw error;
|
|
559
|
+
|
|
560
|
+
case RecoveryStrategy.MANUAL:
|
|
561
|
+
context.state = ExecutionState.PAUSED;
|
|
562
|
+
this.emit('manual-intervention-required', { stepId: step.id, error: error.message });
|
|
563
|
+
return { waitingIntervention: true };
|
|
564
|
+
|
|
565
|
+
default:
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Handle execution-level error
|
|
572
|
+
*/
|
|
573
|
+
async _handleExecutionError(error, context, workflow) {
|
|
574
|
+
const strategy = workflow.errorHandling?.strategy || RecoveryStrategy.ABORT;
|
|
575
|
+
|
|
576
|
+
if (strategy === RecoveryStrategy.ROLLBACK && workflow.errorHandling?.rollbackTo) {
|
|
577
|
+
context.restoreCheckpoint(workflow.errorHandling.rollbackTo);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Log error for analysis
|
|
581
|
+
this.emit('error-logged', {
|
|
582
|
+
executionId: context.executionId,
|
|
583
|
+
error: error.message,
|
|
584
|
+
step: context.currentStep,
|
|
585
|
+
strategy
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Resolve variables in a value
|
|
591
|
+
*/
|
|
592
|
+
_resolveVariables(value, context) {
|
|
593
|
+
if (typeof value === 'string') {
|
|
594
|
+
// Replace ${variable} patterns
|
|
595
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
|
|
596
|
+
const resolved = context.getVariable(varName);
|
|
597
|
+
return resolved !== null ? String(resolved) : '';
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (Array.isArray(value)) {
|
|
602
|
+
return value.map(item => this._resolveVariables(item, context));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (typeof value === 'object' && value !== null) {
|
|
606
|
+
// Check if it's a variable reference
|
|
607
|
+
if (value.$var) {
|
|
608
|
+
return context.getVariable(value.$var, value.default);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const resolved = {};
|
|
612
|
+
for (const [key, val] of Object.entries(value)) {
|
|
613
|
+
resolved[key] = this._resolveVariables(val, context);
|
|
614
|
+
}
|
|
615
|
+
return resolved;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return value;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Evaluate a condition expression
|
|
623
|
+
*/
|
|
624
|
+
_evaluateCondition(condition, context) {
|
|
625
|
+
if (typeof condition === 'boolean') {
|
|
626
|
+
return condition;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (typeof condition === 'object') {
|
|
630
|
+
// Handle comparison operators
|
|
631
|
+
if (condition.$eq) {
|
|
632
|
+
const [left, right] = condition.$eq;
|
|
633
|
+
return this._resolveVariables(left, context) === this._resolveVariables(right, context);
|
|
634
|
+
}
|
|
635
|
+
if (condition.$ne) {
|
|
636
|
+
const [left, right] = condition.$ne;
|
|
637
|
+
return this._resolveVariables(left, context) !== this._resolveVariables(right, context);
|
|
638
|
+
}
|
|
639
|
+
if (condition.$gt) {
|
|
640
|
+
const [left, right] = condition.$gt;
|
|
641
|
+
return this._resolveVariables(left, context) > this._resolveVariables(right, context);
|
|
642
|
+
}
|
|
643
|
+
if (condition.$lt) {
|
|
644
|
+
const [left, right] = condition.$lt;
|
|
645
|
+
return this._resolveVariables(left, context) < this._resolveVariables(right, context);
|
|
646
|
+
}
|
|
647
|
+
if (condition.$exists) {
|
|
648
|
+
const varName = condition.$exists;
|
|
649
|
+
return context.getVariable(varName) !== null;
|
|
650
|
+
}
|
|
651
|
+
if (condition.$and) {
|
|
652
|
+
return condition.$and.every(c => this._evaluateCondition(c, context));
|
|
653
|
+
}
|
|
654
|
+
if (condition.$or) {
|
|
655
|
+
return condition.$or.some(c => this._evaluateCondition(c, context));
|
|
656
|
+
}
|
|
657
|
+
if (condition.$not) {
|
|
658
|
+
return !this._evaluateCondition(condition.$not, context);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// String expression (simple variable truthy check)
|
|
663
|
+
if (typeof condition === 'string') {
|
|
664
|
+
const value = context.getVariable(condition);
|
|
665
|
+
return Boolean(value);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Collect workflow outputs
|
|
673
|
+
*/
|
|
674
|
+
_collectOutputs(context, outputDefs) {
|
|
675
|
+
const outputs = {};
|
|
676
|
+
|
|
677
|
+
for (const outputDef of outputDefs) {
|
|
678
|
+
const name = typeof outputDef === 'string' ? outputDef : outputDef.name;
|
|
679
|
+
const source = typeof outputDef === 'string' ? outputDef : outputDef.from;
|
|
680
|
+
outputs[name] = context.getVariable(source);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return outputs;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Wait for execution to resume
|
|
688
|
+
*/
|
|
689
|
+
async _waitForResume(context) {
|
|
690
|
+
return new Promise(resolve => {
|
|
691
|
+
const checkResume = () => {
|
|
692
|
+
if (context.state === ExecutionState.RUNNING) {
|
|
693
|
+
resolve();
|
|
694
|
+
} else {
|
|
695
|
+
setTimeout(checkResume, 100);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
checkResume();
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Pause execution
|
|
704
|
+
*/
|
|
705
|
+
pause(executionId) {
|
|
706
|
+
const context = this.executions.get(executionId);
|
|
707
|
+
if (context && context.state === ExecutionState.RUNNING) {
|
|
708
|
+
context.state = ExecutionState.PAUSED;
|
|
709
|
+
this.emit('execution-paused', { executionId });
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Resume execution
|
|
717
|
+
*/
|
|
718
|
+
resume(executionId) {
|
|
719
|
+
const context = this.executions.get(executionId);
|
|
720
|
+
if (context && context.state === ExecutionState.PAUSED) {
|
|
721
|
+
context.state = ExecutionState.RUNNING;
|
|
722
|
+
this.emit('execution-resumed', { executionId });
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Cancel execution
|
|
730
|
+
*/
|
|
731
|
+
cancel(executionId) {
|
|
732
|
+
const context = this.executions.get(executionId);
|
|
733
|
+
if (context && [ExecutionState.RUNNING, ExecutionState.PAUSED].includes(context.state)) {
|
|
734
|
+
context.state = ExecutionState.CANCELLED;
|
|
735
|
+
context.endTime = Date.now();
|
|
736
|
+
this.emit('execution-cancelled', { executionId });
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Get execution status
|
|
744
|
+
*/
|
|
745
|
+
getStatus(executionId) {
|
|
746
|
+
const context = this.executions.get(executionId);
|
|
747
|
+
if (!context) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
executionId: context.executionId,
|
|
753
|
+
workflowId: context.workflowId,
|
|
754
|
+
state: context.state,
|
|
755
|
+
currentStep: context.currentStep,
|
|
756
|
+
duration: context.getDuration(),
|
|
757
|
+
stepsCompleted: context.stepResults.size,
|
|
758
|
+
errors: context.errors.map(e => e.message),
|
|
759
|
+
checkpoints: context.checkpoints.map(cp => cp.name)
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Helper sleep function
|
|
765
|
+
*/
|
|
766
|
+
_sleep(ms) {
|
|
767
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
module.exports = {
|
|
772
|
+
WorkflowExecutor,
|
|
773
|
+
WorkflowDefinition,
|
|
774
|
+
ExecutionContext,
|
|
775
|
+
StepResult,
|
|
776
|
+
StepType,
|
|
777
|
+
ExecutionState,
|
|
778
|
+
RecoveryStrategy
|
|
779
|
+
};
|