@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.
@@ -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
- console.log(`🔄 Starting workflow: ${this.config.id} (${executionId})`);
61
- // 🔍 Trace: Workflow Execution
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
- console.log(` ⏭️ Step skipped: ${step.id} (condition not met)`);
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
- console.log(` 📍 Executing step: ${step.id} (${step.type})`);
85
- // 🔍 Trace: Workflow Step (child of workflow_execution)
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); // ✅ Child of workflow_execution
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
- if (step.retryConfig) {
99
- stepResult = await this.executeStepWithRetry(step, currentInput, context);
100
- }
101
- else {
102
- stepResult = await this.executeStep(step, currentInput, context);
103
- }
104
- // Armazenar resultado
105
- context.stepResults.set(step.id, stepResult);
106
- // 🔍 Finalizar trace do step
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
- console.log(`Step completed: ${step.id}`);
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
- // 🔍 Trace de erro do step
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
- // 🔍 Finalizar trace do workflow
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
- console.log(`🎉 Workflow completed: ${this.config.id}`);
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
- console.error(`❌ Workflow failed: ${this.config.id}`, error);
183
- // 🔍 Trace de erro do workflow
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
- console.log(` 🔄 Retry ${attempt}/${retryConfig.maxAttempts} in ${delay}ms: ${step.id}`);
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
- throw new Error(`Agent step failed [${agent.name}/${agent.model}] at step "${context.currentStep}": ${err.message}`);
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 função
298
- const result = await config.execute(input, context);
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
- throw new Error(`Connector step failed [${config.connector}/${config.resource}/${config.action}]: ${err.message}`);
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
- const condition = config.condition(context);
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: results.reduce((acc, result, index) => {
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)