@runflow-ai/sdk 1.0.86 → 1.0.88
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/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/types/all-types.d.ts +130 -2
- package/dist/types/all-types.d.ts.map +1 -1
- package/dist/workflows/index.d.ts +2 -1
- package/dist/workflows/index.d.ts.map +1 -1
- package/dist/workflows/index.js +6 -2
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/workflow.d.ts +49 -3
- package/dist/workflows/workflow.d.ts.map +1 -1
- package/dist/workflows/workflow.js +573 -35
- package/dist/workflows/workflow.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WorkflowBuilder = exports.Workflow = void 0;
|
|
3
|
+
exports.FlowBuilder = exports.WorkflowBuilder = exports.Workflow = void 0;
|
|
4
4
|
exports.createWorkflow = createWorkflow;
|
|
5
5
|
exports.createStep = createStep;
|
|
6
6
|
exports.createAgentStep = createAgentStep;
|
|
7
7
|
exports.createFunctionStep = createFunctionStep;
|
|
8
8
|
exports.createConnectorStep = createConnectorStep;
|
|
9
|
+
exports.flow = flow;
|
|
10
|
+
const events_1 = require("events");
|
|
9
11
|
const agent_1 = require("../core/agent");
|
|
10
12
|
const api_client_1 = require("../core/api-client");
|
|
11
13
|
const trace_collector_1 = require("../observability/trace-collector");
|
|
14
|
+
const logger_1 = require("../core/logger");
|
|
15
|
+
const performance_logger_1 = require("../core/performance-logger");
|
|
16
|
+
const context_1 = require("../core/context");
|
|
12
17
|
// ============================================================================
|
|
13
18
|
// WORKFLOW CLASS
|
|
14
19
|
// ============================================================================
|
|
15
|
-
class Workflow {
|
|
20
|
+
class Workflow extends events_1.EventEmitter {
|
|
16
21
|
constructor(config) {
|
|
22
|
+
super();
|
|
17
23
|
this.config = config;
|
|
24
|
+
this.logger = (0, logger_1.createLogger)({ name: `workflow:${config.id}`, level: this.resolveLogLevel() });
|
|
18
25
|
// Auto-initialize API client when used directly as SDK (outside execution engine)
|
|
19
26
|
if (!this.apiClient) {
|
|
20
27
|
const apiClient = (0, api_client_1.createRunflowAPIClient)();
|
|
@@ -34,6 +41,20 @@ class Workflow {
|
|
|
34
41
|
flushInterval: 1000,
|
|
35
42
|
});
|
|
36
43
|
}
|
|
44
|
+
// Resolve log level from environment
|
|
45
|
+
resolveLogLevel() {
|
|
46
|
+
if (process.env.RUNFLOW_LOG_LEVEL) {
|
|
47
|
+
return process.env.RUNFLOW_LOG_LEVEL;
|
|
48
|
+
}
|
|
49
|
+
return process.env.NODE_ENV === 'production' ? 'info' : 'debug';
|
|
50
|
+
}
|
|
51
|
+
// Save result to both stepResults Map and results plain object
|
|
52
|
+
saveResult(context, stepId, result) {
|
|
53
|
+
context.stepResults.set(stepId, result);
|
|
54
|
+
if (context.results) {
|
|
55
|
+
context.results[stepId] = result;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
37
58
|
// Executar workflow
|
|
38
59
|
async execute(input) {
|
|
39
60
|
if (!this.apiClient) {
|
|
@@ -41,13 +62,18 @@ class Workflow {
|
|
|
41
62
|
}
|
|
42
63
|
// Validar input
|
|
43
64
|
const validatedInput = this.config.inputSchema.parse(input);
|
|
65
|
+
// Resolve execution ID: prefer Runflow global state, fallback to generated
|
|
66
|
+
const globalExecId = context_1.Runflow.getState().executionId;
|
|
67
|
+
const executionId = globalExecId || `wf_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
68
|
+
// Plain object for V2 FlowContext results
|
|
69
|
+
const resultsObj = {};
|
|
44
70
|
// Criar contexto de execução
|
|
45
|
-
const executionId = `wf_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
46
71
|
const context = {
|
|
47
72
|
workflowId: this.config.id,
|
|
48
73
|
executionId,
|
|
49
74
|
input: validatedInput,
|
|
50
75
|
stepResults: new Map(),
|
|
76
|
+
results: resultsObj,
|
|
51
77
|
currentStep: '',
|
|
52
78
|
metadata: {
|
|
53
79
|
startTime: new Date(),
|
|
@@ -57,8 +83,9 @@ class Workflow {
|
|
|
57
83
|
},
|
|
58
84
|
runflowAPI: this.apiClient,
|
|
59
85
|
};
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
this.logger.info({ workflowId: this.config.id, executionId }, `Starting workflow: ${this.config.id} (${executionId})`);
|
|
87
|
+
this.emit('workflow:start', { workflowId: this.config.id, executionId, input: validatedInput });
|
|
88
|
+
// Trace: Workflow Execution
|
|
62
89
|
const workflowStartTime = Date.now();
|
|
63
90
|
const workflowSpan = this.traceCollector?.startSpan('workflow_execution', {
|
|
64
91
|
workflowId: this.config.id,
|
|
@@ -77,12 +104,13 @@ class Workflow {
|
|
|
77
104
|
context.metadata.currentTime = new Date();
|
|
78
105
|
// Verificar condição se existir (antes de contar como executado)
|
|
79
106
|
if (step.condition && !step.condition(context)) {
|
|
80
|
-
|
|
107
|
+
this.logger.debug({ stepId: step.id }, `Step skipped: ${step.id} (condition not met)`);
|
|
108
|
+
this.emit('step:skip', { stepId: step.id, reason: 'condition not met' });
|
|
81
109
|
continue;
|
|
82
110
|
}
|
|
83
111
|
context.metadata.stepCount++;
|
|
84
|
-
|
|
85
|
-
//
|
|
112
|
+
this.logger.debug({ stepId: step.id, stepType: step.type }, `Executing step: ${step.id} (${step.type})`);
|
|
113
|
+
// Trace: Workflow Step (child of workflow_execution)
|
|
86
114
|
const stepStartTime = Date.now();
|
|
87
115
|
const stepSpan = this.traceCollector?.startSpan('workflow_step', {
|
|
88
116
|
workflowId: this.config.id,
|
|
@@ -90,20 +118,23 @@ class Workflow {
|
|
|
90
118
|
stepType: step.type,
|
|
91
119
|
stepNumber: context.metadata.stepCount,
|
|
92
120
|
totalSteps: this.config.steps.length,
|
|
93
|
-
}, workflowSpanId);
|
|
121
|
+
}, workflowSpanId);
|
|
94
122
|
stepSpan?.setInput(currentInput);
|
|
123
|
+
this.emit('step:start', { stepId: step.id, stepType: step.type, input: currentInput });
|
|
95
124
|
// Executar step com retry se configurado
|
|
96
125
|
let stepResult;
|
|
97
126
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
127
|
+
stepResult = await performance_logger_1.performanceLogger.measure(`workflow:${this.config.id}:step:${step.id}`, async () => {
|
|
128
|
+
if (step.retryConfig) {
|
|
129
|
+
return this.executeStepWithRetry(step, currentInput, context);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
return this.executeStep(step, currentInput, context);
|
|
133
|
+
}
|
|
134
|
+
}, { stepType: step.type });
|
|
135
|
+
// Armazenar resultado (both Map and plain object)
|
|
136
|
+
this.saveResult(context, step.id, stepResult);
|
|
137
|
+
// Finalizar trace do step
|
|
107
138
|
stepSpan?.setOutput(stepResult);
|
|
108
139
|
stepSpan?.setMetadata({
|
|
109
140
|
workflowId: this.config.id,
|
|
@@ -125,10 +156,11 @@ class Workflow {
|
|
|
125
156
|
else {
|
|
126
157
|
currentInput = stepResult;
|
|
127
158
|
}
|
|
128
|
-
|
|
159
|
+
this.logger.debug({ stepId: step.id }, `Step completed: ${step.id}`);
|
|
160
|
+
this.emit('step:complete', { stepId: step.id, output: stepResult, durationMs: Date.now() - stepStartTime });
|
|
129
161
|
}
|
|
130
162
|
catch (stepError) {
|
|
131
|
-
//
|
|
163
|
+
// Trace de erro do step
|
|
132
164
|
stepSpan?.setMetadata({
|
|
133
165
|
workflowId: this.config.id,
|
|
134
166
|
stepId: step.id,
|
|
@@ -139,6 +171,7 @@ class Workflow {
|
|
|
139
171
|
error: stepError instanceof Error ? stepError.message : 'Unknown error',
|
|
140
172
|
});
|
|
141
173
|
stepSpan?.finish();
|
|
174
|
+
this.emit('step:error', { stepId: step.id, error: stepError instanceof Error ? stepError.message : 'Unknown error' });
|
|
142
175
|
throw stepError;
|
|
143
176
|
}
|
|
144
177
|
}
|
|
@@ -158,7 +191,7 @@ class Workflow {
|
|
|
158
191
|
};
|
|
159
192
|
// Validar output final
|
|
160
193
|
const finalOutput = this.config.outputSchema.parse(workflowOutput);
|
|
161
|
-
//
|
|
194
|
+
// Finalizar trace do workflow
|
|
162
195
|
workflowSpan?.setOutput({
|
|
163
196
|
stepsExecuted: context.metadata.stepCount,
|
|
164
197
|
totalSteps: this.config.steps.length,
|
|
@@ -175,12 +208,13 @@ class Workflow {
|
|
|
175
208
|
workflowSpan?.finish();
|
|
176
209
|
// Forçar flush dos traces
|
|
177
210
|
await this.traceCollector?.flush();
|
|
178
|
-
|
|
211
|
+
this.logger.info({ workflowId: this.config.id, executionId }, `Workflow completed: ${this.config.id}`);
|
|
212
|
+
this.emit('workflow:complete', { executionId, output: finalOutput, durationMs: Date.now() - workflowStartTime });
|
|
179
213
|
return finalOutput;
|
|
180
214
|
}
|
|
181
215
|
catch (error) {
|
|
182
|
-
|
|
183
|
-
//
|
|
216
|
+
this.logger.error({ workflowId: this.config.id, executionId, err: error }, `Workflow failed: ${this.config.id}`);
|
|
217
|
+
// Trace de erro do workflow
|
|
184
218
|
workflowSpan?.setMetadata({
|
|
185
219
|
workflowId: this.config.id,
|
|
186
220
|
workflowName: this.config.name || this.config.id,
|
|
@@ -193,6 +227,7 @@ class Workflow {
|
|
|
193
227
|
});
|
|
194
228
|
workflowSpan?.finish();
|
|
195
229
|
await this.traceCollector?.flush();
|
|
230
|
+
this.emit('workflow:error', { executionId, error: error instanceof Error ? error.message : 'Unknown error', durationMs: Date.now() - workflowStartTime });
|
|
196
231
|
throw error;
|
|
197
232
|
}
|
|
198
233
|
}
|
|
@@ -209,6 +244,10 @@ class Workflow {
|
|
|
209
244
|
return this.executeConditionStep(step.config, input, context);
|
|
210
245
|
case 'parallel':
|
|
211
246
|
return this.executeParallelStep(step.config, input, context);
|
|
247
|
+
case 'switch':
|
|
248
|
+
return this.executeSwitchStep(step.config, input, context);
|
|
249
|
+
case 'foreach':
|
|
250
|
+
return this.executeForeachStep(step.config, input, context);
|
|
212
251
|
default:
|
|
213
252
|
throw new Error(`Unknown step type: ${step.type}`);
|
|
214
253
|
}
|
|
@@ -230,7 +269,7 @@ class Workflow {
|
|
|
230
269
|
}
|
|
231
270
|
if (attempt < retryConfig.maxAttempts) {
|
|
232
271
|
const delay = this.calculateRetryDelay(retryConfig, attempt);
|
|
233
|
-
|
|
272
|
+
this.logger.info({ stepId: step.id, attempt, maxAttempts: retryConfig.maxAttempts, delay }, `Retry ${attempt}/${retryConfig.maxAttempts} in ${delay}ms: ${step.id}`);
|
|
234
273
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
235
274
|
}
|
|
236
275
|
}
|
|
@@ -268,6 +307,7 @@ class Workflow {
|
|
|
268
307
|
prompt = this.interpolateTemplate(config.promptTemplate, {
|
|
269
308
|
input,
|
|
270
309
|
context: Object.fromEntries(context.stepResults),
|
|
310
|
+
results: context.results || Object.fromEntries(context.stepResults),
|
|
271
311
|
workflow: { id: context.workflowId, step: context.currentStep }
|
|
272
312
|
});
|
|
273
313
|
}
|
|
@@ -285,7 +325,8 @@ class Workflow {
|
|
|
285
325
|
}
|
|
286
326
|
catch (error) {
|
|
287
327
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
288
|
-
|
|
328
|
+
err.message = `Agent step failed [${agent.name}/${agent.model}] at step "${context.currentStep}": ${err.message}`;
|
|
329
|
+
throw err;
|
|
289
330
|
}
|
|
290
331
|
}
|
|
291
332
|
// Executar function step
|
|
@@ -294,8 +335,16 @@ class Workflow {
|
|
|
294
335
|
if (config.inputSchema) {
|
|
295
336
|
input = config.inputSchema.parse(input);
|
|
296
337
|
}
|
|
297
|
-
// Executar
|
|
298
|
-
|
|
338
|
+
// Executar funcao
|
|
339
|
+
let result;
|
|
340
|
+
try {
|
|
341
|
+
result = await config.execute(input, context);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
345
|
+
err.message = `Function step failed at "${context.currentStep}": ${err.message}`;
|
|
346
|
+
throw err;
|
|
347
|
+
}
|
|
299
348
|
// Validar output se schema fornecido
|
|
300
349
|
if (config.outputSchema) {
|
|
301
350
|
return config.outputSchema.parse(result);
|
|
@@ -311,17 +360,27 @@ class Workflow {
|
|
|
311
360
|
}
|
|
312
361
|
catch (error) {
|
|
313
362
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
314
|
-
|
|
363
|
+
err.message = `Connector step failed [${config.connector}/${config.resource}/${config.action}] at step "${context.currentStep}": ${err.message}`;
|
|
364
|
+
throw err;
|
|
315
365
|
}
|
|
316
366
|
}
|
|
317
367
|
// Executar condition step
|
|
318
368
|
async executeConditionStep(config, input, context) {
|
|
319
|
-
|
|
369
|
+
let condition;
|
|
370
|
+
try {
|
|
371
|
+
condition = config.condition(context);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
375
|
+
err.message = `Condition evaluation failed at "${context.currentStep}": ${err.message}`;
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
320
378
|
const pathSteps = condition ? config.truePath : (config.falsePath || []);
|
|
321
379
|
// Executar steps do path escolhido
|
|
322
380
|
let currentInput = input;
|
|
323
381
|
for (const step of pathSteps) {
|
|
324
382
|
currentInput = await this.executeStep(step, currentInput, context);
|
|
383
|
+
this.saveResult(context, step.id, currentInput);
|
|
325
384
|
}
|
|
326
385
|
// Quando unwrapResult=true, retorna apenas o resultado interno (mais natural para downstream)
|
|
327
386
|
if (config.unwrapResult) {
|
|
@@ -335,16 +394,22 @@ class Workflow {
|
|
|
335
394
|
}
|
|
336
395
|
// Executar parallel step
|
|
337
396
|
async executeParallelStep(config, input, context) {
|
|
397
|
+
if (!Array.isArray(config.steps)) {
|
|
398
|
+
throw new Error(`Parallel step "${context.currentStep}" requires an array of steps. ` +
|
|
399
|
+
`Use: .parallel('id', [createFunctionStep('a', handler), createFunctionStep('b', handler)])`);
|
|
400
|
+
}
|
|
338
401
|
const promises = config.steps.map(step => this.executeStep(step, input, context));
|
|
339
402
|
if (config.waitForAll !== false) {
|
|
340
403
|
// Aguardar todos completarem
|
|
341
404
|
const results = await Promise.all(promises);
|
|
405
|
+
const resultsMap = {};
|
|
406
|
+
config.steps.forEach((step, i) => {
|
|
407
|
+
resultsMap[step.id] = results[i];
|
|
408
|
+
this.saveResult(context, step.id, results[i]);
|
|
409
|
+
});
|
|
342
410
|
return {
|
|
343
411
|
type: 'parallel',
|
|
344
|
-
results:
|
|
345
|
-
acc[config.steps[index].id] = result;
|
|
346
|
-
return acc;
|
|
347
|
-
}, {}),
|
|
412
|
+
results: resultsMap,
|
|
348
413
|
};
|
|
349
414
|
}
|
|
350
415
|
else {
|
|
@@ -359,6 +424,92 @@ class Workflow {
|
|
|
359
424
|
};
|
|
360
425
|
}
|
|
361
426
|
}
|
|
427
|
+
// Executar switch step
|
|
428
|
+
async executeSwitchStep(config, input, context) {
|
|
429
|
+
if (typeof config.evaluate !== 'function') {
|
|
430
|
+
const wrongKey = Object.keys(config).find(k => typeof config[k] === 'function' && k !== 'evaluate');
|
|
431
|
+
throw new Error(`Switch step "${context.currentStep}": missing "on" property. ` +
|
|
432
|
+
(wrongKey ? `Did you mean "on" instead of "${wrongKey}"? ` : '') +
|
|
433
|
+
`Use: .switch('id', { on: (ctx) => ctx.results.step.value, cases: { ... } })`);
|
|
434
|
+
}
|
|
435
|
+
const value = config.evaluate(context);
|
|
436
|
+
const handler = config.cases[value] ?? config.defaultCase;
|
|
437
|
+
if (!handler) {
|
|
438
|
+
throw new Error(`Switch step: no case matched for "${value}" and no default provided. Available cases: [${Object.keys(config.cases).join(', ')}]`);
|
|
439
|
+
}
|
|
440
|
+
this.logger.debug({ switchValue: value }, `Switch routing: "${value}"`);
|
|
441
|
+
let result;
|
|
442
|
+
try {
|
|
443
|
+
if (Array.isArray(handler)) {
|
|
444
|
+
let currentInput = input;
|
|
445
|
+
for (const step of handler) {
|
|
446
|
+
currentInput = await this.executeStep(step, currentInput, context);
|
|
447
|
+
this.saveResult(context, step.id, currentInput);
|
|
448
|
+
}
|
|
449
|
+
result = currentInput;
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
result = await handler(input, context);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
457
|
+
err.message = `Switch step failed at "${context.currentStep}" (case "${value}"): ${err.message}`;
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
if (config.unwrapResult ?? true) {
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
return { switchValue: value, caseTaken: config.cases[value] ? value : 'default', result };
|
|
464
|
+
}
|
|
465
|
+
// Executar foreach step
|
|
466
|
+
async executeForeachStep(config, input, context) {
|
|
467
|
+
if (typeof config.handler !== 'function') {
|
|
468
|
+
throw new Error(`Foreach step "${context.currentStep}": missing "handler" function. ` +
|
|
469
|
+
`Use: .foreach('id', { handler: async (item) => ({ ...item, processed: true }) })`);
|
|
470
|
+
}
|
|
471
|
+
if (!Array.isArray(input)) {
|
|
472
|
+
this.logger.warn({ stepId: context.currentStep, inputType: typeof input }, `Foreach step "${context.currentStep}" received non-array input (${typeof input}). ` +
|
|
473
|
+
`The previous step must return an array. Use .map() to extract the array first.`);
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
const items = input;
|
|
477
|
+
if (items.length === 0)
|
|
478
|
+
return [];
|
|
479
|
+
const concurrency = config.concurrency || 1;
|
|
480
|
+
const handleItem = async (item, itemIndex) => {
|
|
481
|
+
try {
|
|
482
|
+
return await config.handler(item, context);
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
486
|
+
err.message = `Foreach step failed at "${context.currentStep}" (item ${itemIndex}): ${err.message}`;
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
if (concurrency === 1) {
|
|
491
|
+
const results = [];
|
|
492
|
+
for (let i = 0; i < items.length; i++) {
|
|
493
|
+
results.push(await handleItem(items[i], i));
|
|
494
|
+
}
|
|
495
|
+
return results;
|
|
496
|
+
}
|
|
497
|
+
// Concurrent with limit.
|
|
498
|
+
// The shared `index` variable is safe because Node.js is single-threaded:
|
|
499
|
+
// `index++` (read + increment) completes synchronously before the `await`,
|
|
500
|
+
// so no two workers can read the same index value.
|
|
501
|
+
const results = new Array(items.length);
|
|
502
|
+
let index = 0;
|
|
503
|
+
const runNext = async () => {
|
|
504
|
+
while (index < items.length) {
|
|
505
|
+
const currentIndex = index++;
|
|
506
|
+
results[currentIndex] = await handleItem(items[currentIndex], currentIndex);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());
|
|
510
|
+
await Promise.all(workers);
|
|
511
|
+
return results;
|
|
512
|
+
}
|
|
362
513
|
// Interpolar template com variáveis
|
|
363
514
|
interpolateTemplate(template, variables) {
|
|
364
515
|
return template.replace(/\{\{([\w-]+(?:\.[\w-]+)*)\}\}/g, (match, path) => {
|
|
@@ -391,6 +542,231 @@ class Workflow {
|
|
|
391
542
|
getNestedValue(obj, path) {
|
|
392
543
|
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
393
544
|
}
|
|
545
|
+
// ============================================================================
|
|
546
|
+
// GRAPH GENERATION
|
|
547
|
+
// ============================================================================
|
|
548
|
+
toGraph() {
|
|
549
|
+
const nodes = [];
|
|
550
|
+
const edges = [];
|
|
551
|
+
let previousExitNodes = [];
|
|
552
|
+
for (const step of this.config.steps) {
|
|
553
|
+
const entryNodes = this.addStepToGraph(step, nodes, edges);
|
|
554
|
+
// Connect previous step's exit nodes to this step's entry nodes
|
|
555
|
+
for (const prev of previousExitNodes) {
|
|
556
|
+
for (const entry of entryNodes) {
|
|
557
|
+
edges.push({ source: prev, target: entry });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
previousExitNodes = this.getExitNodes(step);
|
|
561
|
+
}
|
|
562
|
+
return { id: this.config.id, name: this.config.name || this.config.id, nodes, edges };
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Adds a step (and its sub-steps) to the graph. Returns the entry node IDs.
|
|
566
|
+
*/
|
|
567
|
+
addStepToGraph(step, nodes, edges) {
|
|
568
|
+
const nodeType = this.stepTypeToNodeType(step.type);
|
|
569
|
+
if (step.type === 'condition') {
|
|
570
|
+
const config = step.config;
|
|
571
|
+
// Add the branch decision node
|
|
572
|
+
nodes.push({
|
|
573
|
+
id: step.id,
|
|
574
|
+
type: nodeType,
|
|
575
|
+
label: step.name || step.id,
|
|
576
|
+
...(step.description && { metadata: { description: step.description } }),
|
|
577
|
+
});
|
|
578
|
+
// True path sub-nodes
|
|
579
|
+
let prevIds = [step.id];
|
|
580
|
+
for (const sub of config.truePath) {
|
|
581
|
+
const subEntries = this.addStepToGraph(sub, nodes, edges);
|
|
582
|
+
for (const prev of prevIds) {
|
|
583
|
+
for (const entry of subEntries) {
|
|
584
|
+
edges.push({
|
|
585
|
+
source: prev,
|
|
586
|
+
target: entry,
|
|
587
|
+
label: prev === step.id ? 'true' : undefined,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
prevIds = this.getExitNodes(sub);
|
|
592
|
+
}
|
|
593
|
+
// False path sub-nodes
|
|
594
|
+
if (config.falsePath && config.falsePath.length > 0) {
|
|
595
|
+
let falsePrevIds = [step.id];
|
|
596
|
+
for (const sub of config.falsePath) {
|
|
597
|
+
const subEntries = this.addStepToGraph(sub, nodes, edges);
|
|
598
|
+
for (const prev of falsePrevIds) {
|
|
599
|
+
for (const entry of subEntries) {
|
|
600
|
+
edges.push({
|
|
601
|
+
source: prev,
|
|
602
|
+
target: entry,
|
|
603
|
+
label: prev === step.id ? 'false' : undefined,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
falsePrevIds = this.getExitNodes(sub);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return [step.id];
|
|
611
|
+
}
|
|
612
|
+
if (step.type === 'switch') {
|
|
613
|
+
const config = step.config;
|
|
614
|
+
nodes.push({
|
|
615
|
+
id: step.id,
|
|
616
|
+
type: nodeType,
|
|
617
|
+
label: step.name || step.id,
|
|
618
|
+
...(step.description && { metadata: { description: step.description } }),
|
|
619
|
+
});
|
|
620
|
+
// Add case nodes
|
|
621
|
+
for (const [caseName, handler] of Object.entries(config.cases)) {
|
|
622
|
+
if (Array.isArray(handler)) {
|
|
623
|
+
// handler is WorkflowStep[]
|
|
624
|
+
let prevIds = [step.id];
|
|
625
|
+
for (const sub of handler) {
|
|
626
|
+
const subEntries = this.addStepToGraph(sub, nodes, edges);
|
|
627
|
+
for (const prev of prevIds) {
|
|
628
|
+
for (const entry of subEntries) {
|
|
629
|
+
edges.push({
|
|
630
|
+
source: prev,
|
|
631
|
+
target: entry,
|
|
632
|
+
label: prev === step.id ? caseName : undefined,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
prevIds = this.getExitNodes(sub);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// handler is a function — create virtual node
|
|
641
|
+
const virtualId = `${step.id}:${caseName}`;
|
|
642
|
+
nodes.push({ id: virtualId, type: 'function', label: caseName });
|
|
643
|
+
edges.push({ source: step.id, target: virtualId, label: caseName });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Default case
|
|
647
|
+
if (config.defaultCase) {
|
|
648
|
+
if (Array.isArray(config.defaultCase)) {
|
|
649
|
+
let prevIds = [step.id];
|
|
650
|
+
for (const sub of config.defaultCase) {
|
|
651
|
+
const subEntries = this.addStepToGraph(sub, nodes, edges);
|
|
652
|
+
for (const prev of prevIds) {
|
|
653
|
+
for (const entry of subEntries) {
|
|
654
|
+
edges.push({
|
|
655
|
+
source: prev,
|
|
656
|
+
target: entry,
|
|
657
|
+
label: prev === step.id ? 'default' : undefined,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
prevIds = this.getExitNodes(sub);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
const virtualId = `${step.id}:default`;
|
|
666
|
+
nodes.push({ id: virtualId, type: 'function', label: 'default' });
|
|
667
|
+
edges.push({ source: step.id, target: virtualId, label: 'default' });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return [step.id];
|
|
671
|
+
}
|
|
672
|
+
if (step.type === 'parallel') {
|
|
673
|
+
const config = step.config;
|
|
674
|
+
// Add container node
|
|
675
|
+
nodes.push({
|
|
676
|
+
id: step.id,
|
|
677
|
+
type: nodeType,
|
|
678
|
+
label: step.name || step.id,
|
|
679
|
+
...(step.description && { metadata: { description: step.description } }),
|
|
680
|
+
});
|
|
681
|
+
// Add child nodes
|
|
682
|
+
for (const sub of config.steps) {
|
|
683
|
+
const subEntries = this.addStepToGraph(sub, nodes, edges);
|
|
684
|
+
for (const entry of subEntries) {
|
|
685
|
+
edges.push({ source: step.id, target: entry });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return [step.id];
|
|
689
|
+
}
|
|
690
|
+
// Simple step (function, agent, connector, foreach)
|
|
691
|
+
nodes.push({
|
|
692
|
+
id: step.id,
|
|
693
|
+
type: nodeType,
|
|
694
|
+
label: step.name || step.id,
|
|
695
|
+
...(step.description && { metadata: { description: step.description } }),
|
|
696
|
+
});
|
|
697
|
+
return [step.id];
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Maps step type to GraphNode type. 'function' becomes 'step' for display purposes,
|
|
701
|
+
* but we keep the original type for graph fidelity.
|
|
702
|
+
*/
|
|
703
|
+
stepTypeToNodeType(type) {
|
|
704
|
+
return type;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Returns the IDs of nodes that "exit" a complex step — i.e., the last nodes
|
|
708
|
+
* in each branch that need to connect to the next sequential step.
|
|
709
|
+
*/
|
|
710
|
+
getExitNodes(step) {
|
|
711
|
+
if (step.type === 'condition') {
|
|
712
|
+
const config = step.config;
|
|
713
|
+
const exits = [];
|
|
714
|
+
if (config.truePath.length > 0) {
|
|
715
|
+
const last = config.truePath[config.truePath.length - 1];
|
|
716
|
+
exits.push(...this.getExitNodes(last));
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
exits.push(step.id);
|
|
720
|
+
}
|
|
721
|
+
if (config.falsePath && config.falsePath.length > 0) {
|
|
722
|
+
const last = config.falsePath[config.falsePath.length - 1];
|
|
723
|
+
exits.push(...this.getExitNodes(last));
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
exits.push(step.id);
|
|
727
|
+
}
|
|
728
|
+
return exits;
|
|
729
|
+
}
|
|
730
|
+
if (step.type === 'switch') {
|
|
731
|
+
const config = step.config;
|
|
732
|
+
const exits = [];
|
|
733
|
+
for (const [caseName, handler] of Object.entries(config.cases)) {
|
|
734
|
+
if (Array.isArray(handler) && handler.length > 0) {
|
|
735
|
+
const last = handler[handler.length - 1];
|
|
736
|
+
exits.push(...this.getExitNodes(last));
|
|
737
|
+
}
|
|
738
|
+
else if (!Array.isArray(handler)) {
|
|
739
|
+
exits.push(`${step.id}:${caseName}`);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
exits.push(step.id);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (config.defaultCase) {
|
|
746
|
+
if (Array.isArray(config.defaultCase) && config.defaultCase.length > 0) {
|
|
747
|
+
const last = config.defaultCase[config.defaultCase.length - 1];
|
|
748
|
+
exits.push(...this.getExitNodes(last));
|
|
749
|
+
}
|
|
750
|
+
else if (!Array.isArray(config.defaultCase)) {
|
|
751
|
+
exits.push(`${step.id}:default`);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
exits.push(step.id);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return exits;
|
|
758
|
+
}
|
|
759
|
+
if (step.type === 'parallel') {
|
|
760
|
+
const config = step.config;
|
|
761
|
+
const exits = [];
|
|
762
|
+
for (const sub of config.steps) {
|
|
763
|
+
exits.push(...this.getExitNodes(sub));
|
|
764
|
+
}
|
|
765
|
+
return exits.length > 0 ? exits : [step.id];
|
|
766
|
+
}
|
|
767
|
+
// Simple step
|
|
768
|
+
return [step.id];
|
|
769
|
+
}
|
|
394
770
|
// Getters
|
|
395
771
|
get id() {
|
|
396
772
|
return this.config.id;
|
|
@@ -488,7 +864,7 @@ class WorkflowBuilder {
|
|
|
488
864
|
return this.then(parallelStep);
|
|
489
865
|
}
|
|
490
866
|
// Adicionar conditional step
|
|
491
|
-
condition(id, condition, truePath, falsePath) {
|
|
867
|
+
condition(id, condition, truePath, falsePath, options) {
|
|
492
868
|
const conditionStep = {
|
|
493
869
|
id,
|
|
494
870
|
type: 'condition',
|
|
@@ -496,6 +872,7 @@ class WorkflowBuilder {
|
|
|
496
872
|
condition,
|
|
497
873
|
truePath,
|
|
498
874
|
falsePath,
|
|
875
|
+
unwrapResult: options?.unwrap,
|
|
499
876
|
},
|
|
500
877
|
};
|
|
501
878
|
return this.then(conditionStep);
|
|
@@ -520,6 +897,163 @@ class WorkflowBuilder {
|
|
|
520
897
|
}
|
|
521
898
|
exports.WorkflowBuilder = WorkflowBuilder;
|
|
522
899
|
// ============================================================================
|
|
900
|
+
// FLOW BUILDER (V2 Fluent API)
|
|
901
|
+
// ============================================================================
|
|
902
|
+
class FlowBuilder {
|
|
903
|
+
constructor(config) {
|
|
904
|
+
this.steps = [];
|
|
905
|
+
this.stepCounter = 0;
|
|
906
|
+
this.config = config;
|
|
907
|
+
}
|
|
908
|
+
nextId(prefix) {
|
|
909
|
+
return `${prefix}_${++this.stepCounter}`;
|
|
910
|
+
}
|
|
911
|
+
step(id, handlerOrOpts) {
|
|
912
|
+
let handler;
|
|
913
|
+
let opts = {};
|
|
914
|
+
if (typeof handlerOrOpts === 'function') {
|
|
915
|
+
handler = handlerOrOpts;
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
handler = handlerOrOpts.handler;
|
|
919
|
+
opts = handlerOrOpts;
|
|
920
|
+
}
|
|
921
|
+
const step = {
|
|
922
|
+
id,
|
|
923
|
+
type: 'function',
|
|
924
|
+
config: {
|
|
925
|
+
execute: handler,
|
|
926
|
+
outputSchema: opts.outputSchema,
|
|
927
|
+
},
|
|
928
|
+
condition: opts.condition,
|
|
929
|
+
retryConfig: opts.retryConfig,
|
|
930
|
+
inputTransform: opts.inputTransform,
|
|
931
|
+
outputTransform: opts.outputTransform,
|
|
932
|
+
};
|
|
933
|
+
this.steps.push(step);
|
|
934
|
+
return this;
|
|
935
|
+
}
|
|
936
|
+
agent(id, agent, opts) {
|
|
937
|
+
const step = {
|
|
938
|
+
id,
|
|
939
|
+
type: 'agent',
|
|
940
|
+
config: {
|
|
941
|
+
agent,
|
|
942
|
+
prompt: opts?.prompt,
|
|
943
|
+
promptTemplate: opts?.promptTemplate,
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
this.steps.push(step);
|
|
947
|
+
return this;
|
|
948
|
+
}
|
|
949
|
+
branch(id, opts) {
|
|
950
|
+
const step = {
|
|
951
|
+
id,
|
|
952
|
+
type: 'condition',
|
|
953
|
+
config: {
|
|
954
|
+
condition: opts.condition,
|
|
955
|
+
truePath: this.toBranchPath(`${id}_true`, opts.onTrue),
|
|
956
|
+
falsePath: opts.onFalse ? this.toBranchPath(`${id}_false`, opts.onFalse) : undefined,
|
|
957
|
+
unwrapResult: opts.unwrap ?? true,
|
|
958
|
+
},
|
|
959
|
+
};
|
|
960
|
+
this.steps.push(step);
|
|
961
|
+
return this;
|
|
962
|
+
}
|
|
963
|
+
switch(id, opts) {
|
|
964
|
+
const step = {
|
|
965
|
+
id,
|
|
966
|
+
type: 'switch',
|
|
967
|
+
config: {
|
|
968
|
+
evaluate: opts.on,
|
|
969
|
+
cases: opts.cases,
|
|
970
|
+
defaultCase: opts.default,
|
|
971
|
+
unwrapResult: opts.unwrap ?? true,
|
|
972
|
+
},
|
|
973
|
+
};
|
|
974
|
+
this.steps.push(step);
|
|
975
|
+
return this;
|
|
976
|
+
}
|
|
977
|
+
map(transform) {
|
|
978
|
+
if (typeof transform !== 'function') {
|
|
979
|
+
throw new Error(`.map() expects a transform function, not ${typeof transform}. ` +
|
|
980
|
+
`Use: .map((output) => output.items) — no ID parameter, no ctx.`);
|
|
981
|
+
}
|
|
982
|
+
const id = this.nextId('map');
|
|
983
|
+
const step = {
|
|
984
|
+
id,
|
|
985
|
+
type: 'function',
|
|
986
|
+
config: {
|
|
987
|
+
execute: async (input) => transform(input),
|
|
988
|
+
},
|
|
989
|
+
};
|
|
990
|
+
this.steps.push(step);
|
|
991
|
+
return this;
|
|
992
|
+
}
|
|
993
|
+
foreach(id, opts) {
|
|
994
|
+
const step = {
|
|
995
|
+
id,
|
|
996
|
+
type: 'foreach',
|
|
997
|
+
config: {
|
|
998
|
+
handler: opts.handler,
|
|
999
|
+
concurrency: opts.concurrency,
|
|
1000
|
+
},
|
|
1001
|
+
};
|
|
1002
|
+
this.steps.push(step);
|
|
1003
|
+
return this;
|
|
1004
|
+
}
|
|
1005
|
+
connector(id, connector, resource, action, parameters) {
|
|
1006
|
+
const step = {
|
|
1007
|
+
id,
|
|
1008
|
+
type: 'connector',
|
|
1009
|
+
config: {
|
|
1010
|
+
connector,
|
|
1011
|
+
resource,
|
|
1012
|
+
action,
|
|
1013
|
+
parameters,
|
|
1014
|
+
},
|
|
1015
|
+
};
|
|
1016
|
+
this.steps.push(step);
|
|
1017
|
+
return this;
|
|
1018
|
+
}
|
|
1019
|
+
parallel(id, steps, opts) {
|
|
1020
|
+
const step = {
|
|
1021
|
+
id,
|
|
1022
|
+
type: 'parallel',
|
|
1023
|
+
config: {
|
|
1024
|
+
steps,
|
|
1025
|
+
waitForAll: opts?.waitForAll,
|
|
1026
|
+
maxConcurrency: opts?.maxConcurrency,
|
|
1027
|
+
},
|
|
1028
|
+
};
|
|
1029
|
+
this.steps.push(step);
|
|
1030
|
+
return this;
|
|
1031
|
+
}
|
|
1032
|
+
output(transform) {
|
|
1033
|
+
this.config = { ...this.config, outputTransform: transform };
|
|
1034
|
+
return this;
|
|
1035
|
+
}
|
|
1036
|
+
build() {
|
|
1037
|
+
return new Workflow({ ...this.config, steps: this.steps });
|
|
1038
|
+
}
|
|
1039
|
+
toBranchPath(defaultId, handlerOrSteps) {
|
|
1040
|
+
if (Array.isArray(handlerOrSteps)) {
|
|
1041
|
+
return handlerOrSteps;
|
|
1042
|
+
}
|
|
1043
|
+
return [{
|
|
1044
|
+
id: defaultId,
|
|
1045
|
+
type: 'function',
|
|
1046
|
+
config: {
|
|
1047
|
+
execute: handlerOrSteps,
|
|
1048
|
+
},
|
|
1049
|
+
}];
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
exports.FlowBuilder = FlowBuilder;
|
|
1053
|
+
function flow(config) {
|
|
1054
|
+
return new FlowBuilder(config);
|
|
1055
|
+
}
|
|
1056
|
+
// ============================================================================
|
|
523
1057
|
// HELPER FUNCTIONS
|
|
524
1058
|
// ============================================================================
|
|
525
1059
|
function inferStepType(config) {
|
|
@@ -527,6 +1061,10 @@ function inferStepType(config) {
|
|
|
527
1061
|
return 'agent';
|
|
528
1062
|
if ('execute' in config)
|
|
529
1063
|
return 'function';
|
|
1064
|
+
if ('evaluate' in config)
|
|
1065
|
+
return 'switch';
|
|
1066
|
+
if ('handler' in config && !('execute' in config))
|
|
1067
|
+
return 'foreach';
|
|
530
1068
|
if ('connector' in config)
|
|
531
1069
|
return 'connector';
|
|
532
1070
|
if ('condition' in config)
|