@o-lang/olang 1.0.26 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/cli.js +125 -133
  2. package/package.json +1 -1
  3. package/src/parser.js +432 -94
  4. package/src/runtime.js +320 -42
package/src/parser.js CHANGED
@@ -1,11 +1,23 @@
1
1
  const fs = require('fs');
2
2
 
3
+ // ✅ Symbol normalization helper (backward compatible)
4
+ function normalizeSymbol(raw) {
5
+ if (!raw) return raw;
6
+ // Take only the first word (stop at first whitespace)
7
+ // Keep letters, numbers, underscores, and $ (for JS compatibility)
8
+ return raw.split(/\s+/)[0].replace(/[^\w$]/g, '');
9
+ }
10
+
3
11
  function parse(content, filename = '<unknown>') {
4
12
  if (typeof content === 'string') {
13
+ // ✅ Strip UTF-8 BOM if present (0xFEFF = Unicode BOM)
14
+ if (content.charCodeAt(0) === 0xFEFF) {
15
+ content = content.slice(1);
16
+ }
17
+
5
18
  const lines = content.split('\n').map(line => line.replace(/\r$/, ''));
6
19
  return parseLines(lines, filename);
7
20
  } else if (typeof content === 'object' && content !== null) {
8
- // Already parsed
9
21
  return content;
10
22
  } else {
11
23
  throw new Error('parse() expects string content or pre-parsed object');
@@ -13,12 +25,15 @@ function parse(content, filename = '<unknown>') {
13
25
  }
14
26
 
15
27
  function parseFromFile(filepath) {
28
+ // Enforce .ol extension
29
+ if (!filepath.endsWith(".ol")) {
30
+ throw new Error(`Expected .ol workflow, got: ${filepath}`);
31
+ }
16
32
  const content = fs.readFileSync(filepath, 'utf8');
17
33
  return parse(content, filepath);
18
34
  }
19
35
 
20
36
  function parseLines(lines, filename) {
21
- // Remove evolution file parsing - evolution is now in-workflow
22
37
  return parseWorkflowLines(lines, filename);
23
38
  }
24
39
 
@@ -30,27 +45,40 @@ function parseWorkflowLines(lines, filename) {
30
45
  steps: [],
31
46
  returnValues: [],
32
47
  allowedResolvers: [],
33
- maxGenerations: null, // ✅ Updated field name for Constraint: max_generations = X
48
+ maxGenerations: null,
34
49
  __warnings: [],
35
50
  filename: filename
36
51
  };
37
-
52
+
38
53
  let i = 0;
39
54
  let currentStep = null;
40
55
  let inAllowResolvers = false;
41
56
  let inIfBlock = false;
42
57
  let ifCondition = null;
43
58
  let ifBody = [];
44
-
59
+ let inParallelBlock = false;
60
+ let parallelSteps = [];
61
+ let parallelTimeout = null;
62
+ let inEscalationBlock = false;
63
+ let escalationLevels = [];
64
+ let currentLevel = null;
65
+
66
+ // ✅ Helper to flush currentStep
67
+ const flushCurrentStep = () => {
68
+ if (currentStep) {
69
+ workflow.steps.push(currentStep);
70
+ currentStep = null;
71
+ }
72
+ };
73
+
45
74
  while (i < lines.length) {
46
75
  let line = lines[i++].trim();
47
-
48
- // Skip empty lines and comments
76
+
49
77
  if (line === '' || line.startsWith('#')) {
50
78
  continue;
51
79
  }
52
-
53
- // Parse Workflow declaration
80
+
81
+ // Workflow declaration
54
82
  if (line.startsWith('Workflow ')) {
55
83
  const match = line.match(/^Workflow\s+"([^"]+)"(?:\s+with\s+(.+))?$/i);
56
84
  if (match) {
@@ -63,8 +91,8 @@ function parseWorkflowLines(lines, filename) {
63
91
  }
64
92
  continue;
65
93
  }
66
-
67
- // Parse Constraint: max_generations = X (✅ Updated syntax)
94
+
95
+ // Global Constraint: max_generations
68
96
  if (line.startsWith('Constraint: max_generations = ')) {
69
97
  const match = line.match(/^Constraint:\s+max_generations\s*=\s*(\d+)$/i);
70
98
  if (match) {
@@ -74,13 +102,13 @@ function parseWorkflowLines(lines, filename) {
74
102
  }
75
103
  continue;
76
104
  }
77
-
78
- // Parse Allow resolvers section
105
+
106
+ // Allow resolvers section
79
107
  if (line === 'Allow resolvers:') {
80
108
  inAllowResolvers = true;
81
109
  continue;
82
110
  }
83
-
111
+
84
112
  if (inAllowResolvers) {
85
113
  if (line.startsWith('- ')) {
86
114
  const resolverName = line.substring(2).trim();
@@ -88,28 +116,153 @@ function parseWorkflowLines(lines, filename) {
88
116
  workflow.allowedResolvers.push(resolverName);
89
117
  }
90
118
  } else if (line === '' || line.startsWith('#')) {
91
- // Continue
119
+ continue;
92
120
  } else {
93
- // End of Allow resolvers section
94
121
  inAllowResolvers = false;
95
- i--; // Re-process this line
122
+ i--;
96
123
  continue;
97
124
  }
98
125
  continue;
99
126
  }
100
-
101
- // Parse Step declarations
102
- const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
103
- if (stepMatch) {
104
- // Save previous step if it exists
105
- if (currentStep) {
106
- workflow.steps.push(currentStep);
107
- currentStep = null;
127
+
128
+ // Parse Escalation block
129
+ if (line.match(/^Run in parallel with escalation:$/i)) {
130
+ flushCurrentStep();
131
+ inEscalationBlock = true;
132
+ escalationLevels = [];
133
+ currentLevel = null;
134
+ continue;
135
+ }
136
+
137
+ if (inEscalationBlock) {
138
+ if (line.match(/^End$/i)) {
139
+ if (currentLevel) {
140
+ escalationLevels.push(currentLevel);
141
+ }
142
+ workflow.steps.push({
143
+ type: 'escalation',
144
+ levels: escalationLevels,
145
+ stepNumber: workflow.steps.length + 1
146
+ });
147
+ inEscalationBlock = false;
148
+ continue;
149
+ } else if (line.match(/^Level \d+:/i)) {
150
+ // Parse level declaration
151
+ const levelMatch = line.match(/^Level (\d+):\s+(.+)$/i);
152
+ if (levelMatch) {
153
+ if (currentLevel) {
154
+ escalationLevels.push(currentLevel);
155
+ }
156
+
157
+ // Parse timeout from level description
158
+ let timeoutMs = null;
159
+ const desc = levelMatch[2].trim().toLowerCase();
160
+ if (desc.includes('immediately')) {
161
+ timeoutMs = 0;
162
+ } else {
163
+ const timeMatch = desc.match(/within\s+(\d+)\s*([smhd])/i);
164
+ if (timeMatch) {
165
+ const value = parseInt(timeMatch[1]);
166
+ const unit = timeMatch[2].toLowerCase();
167
+ const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
168
+ timeoutMs = value * (multipliers[unit] || 1000);
169
+ }
170
+ }
171
+
172
+ currentLevel = {
173
+ levelNumber: parseInt(levelMatch[1]),
174
+ timeout: timeoutMs,
175
+ steps: []
176
+ };
177
+ continue;
178
+ }
179
+ } else if (currentLevel) {
180
+ // Parse steps within level
181
+ currentLevel.steps.push(line);
182
+ continue;
108
183
  }
184
+ }
185
+
186
+ // ✅ Parse Timed Parallel block (EXACT FORMAT - NO DUPLICATION)
187
+ const timedParMatch = line.match(/^Run in parallel for (\d+)\s*([smhd])$/i);
188
+ if (timedParMatch) {
189
+ flushCurrentStep();
190
+
191
+ const value = parseInt(timedParMatch[1]);
192
+ const unit = timedParMatch[2].toLowerCase();
193
+ const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
194
+ const timeoutMs = value * (multipliers[unit] || 1000);
109
195
 
196
+ inParallelBlock = true;
197
+ parallelSteps = [];
198
+ parallelTimeout = timeoutMs;
199
+ continue;
200
+ }
201
+
202
+ // ✅ Parse Normal Parallel block (backward compatible)
203
+ if (line.match(/^Run in parallel$/i)) {
204
+ flushCurrentStep();
205
+ inParallelBlock = true;
206
+ parallelSteps = [];
207
+ parallelTimeout = null;
208
+ continue;
209
+ }
210
+
211
+ if (inParallelBlock) {
212
+ if (line.match(/^End$/i)) {
213
+ flushCurrentStep(); // ✅ Flush last parallel step
214
+ const parsedParallel = parseBlock(parallelSteps);
215
+ workflow.steps.push({
216
+ type: 'parallel',
217
+ steps: parsedParallel,
218
+ timeout: parallelTimeout,
219
+ stepNumber: workflow.steps.length + 1
220
+ });
221
+ inParallelBlock = false;
222
+ parallelTimeout = null;
223
+ continue;
224
+ } else {
225
+ parallelSteps.push(line);
226
+ continue;
227
+ }
228
+ }
229
+
230
+ // ✅ FLUSH before If/When block
231
+ if (line.match(/^(?:If|When)\s+(.+)$/i)) {
232
+ flushCurrentStep();
233
+ const ifMatch = line.match(/^(?:If|When)\s+(.+)$/i);
234
+ ifCondition = ifMatch[1].trim();
235
+ inIfBlock = true;
236
+ ifBody = [];
237
+ continue;
238
+ }
239
+
240
+ if (inIfBlock) {
241
+ if (line.match(/^End(?:If)?$/i)) {
242
+ flushCurrentStep(); // ✅ Flush last if step
243
+ const parsedIfBody = parseBlock(ifBody);
244
+ workflow.steps.push({
245
+ type: 'if',
246
+ condition: ifCondition,
247
+ body: parsedIfBody,
248
+ stepNumber: workflow.steps.length + 1
249
+ });
250
+ inIfBlock = false;
251
+ ifCondition = null;
252
+ ifBody = [];
253
+ continue;
254
+ } else {
255
+ ifBody.push(line);
256
+ continue;
257
+ }
258
+ }
259
+
260
+ // Step declaration
261
+ const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
262
+ if (stepMatch) {
263
+ flushCurrentStep(); // ✅ Flush previous step
110
264
  const stepNumber = parseInt(stepMatch[1], 10);
111
265
  const stepContent = stepMatch[2];
112
-
113
266
  currentStep = {
114
267
  type: 'action',
115
268
  stepNumber: stepNumber,
@@ -119,81 +272,138 @@ function parseWorkflowLines(lines, filename) {
119
272
  };
120
273
  continue;
121
274
  }
122
-
123
- // Parse Evolve steps (NEW IN-WORKFLOW EVOLUTION)
275
+
276
+ // Save as -Apply normalization
277
+ const saveMatch = line.match(/^Save as\s+(.+)$/i);
278
+ if (saveMatch && currentStep) {
279
+ currentStep.saveAs = normalizeSymbol(saveMatch[1].trim());
280
+ continue;
281
+ }
282
+
283
+ // Constraint (per-step)
284
+ const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
285
+ if (constraintMatch && currentStep) {
286
+ const constraintLine = constraintMatch[1].trim();
287
+ const eq = constraintLine.match(/^([^=]+)=\s*(.+)$/);
288
+ if (eq) {
289
+ let key = eq[1].trim();
290
+ let value = eq[2].trim();
291
+
292
+ if (value.startsWith('[') && value.endsWith(']')) {
293
+ value = value.slice(1, -1).split(',').map(v => v.trim().replace(/^"/, '').replace(/"$/, ''));
294
+ } else if (!isNaN(value)) {
295
+ value = Number(value);
296
+ } else if (value.startsWith('"') && value.endsWith('"')) {
297
+ value = value.slice(1, -1);
298
+ }
299
+
300
+ currentStep.constraints[key] = value;
301
+ }
302
+ continue;
303
+ }
304
+
305
+ // Debrief
306
+ const debriefMatch = line.match(/^Debrief\s+([^\s]+)\s+with\s+"([^"]*)"$/i);
307
+ if (debriefMatch) {
308
+ flushCurrentStep();
309
+ workflow.steps.push({
310
+ type: 'debrief',
311
+ agent: debriefMatch[1].trim(),
312
+ message: debriefMatch[2],
313
+ stepNumber: workflow.steps.length + 1
314
+ });
315
+ continue;
316
+ }
317
+
318
+ // Evolve
124
319
  const evolveMatch = line.match(/^Evolve\s+([^\s]+)\s+using\s+feedback:\s*"([^"]*)"$/i);
125
320
  if (evolveMatch) {
126
- if (currentStep) {
127
- workflow.steps.push(currentStep);
128
- currentStep = null;
129
- }
130
-
131
- currentStep = {
321
+ flushCurrentStep();
322
+ workflow.steps.push({
132
323
  type: 'evolve',
133
- stepNumber: workflow.steps.length + 1,
134
324
  targetResolver: evolveMatch[1].trim(),
135
325
  feedback: evolveMatch[2],
136
- saveAs: null,
137
- constraints: {}
138
- };
326
+ stepNumber: workflow.steps.length + 1
327
+ });
139
328
  continue;
140
329
  }
141
-
142
- // Parse Save as
143
- const saveMatch = line.match(/^Save as\s+(.+)$/i);
144
- if (saveMatch && currentStep) {
145
- currentStep.saveAs = saveMatch[1].trim();
330
+
331
+ // Prompt
332
+ const promptMatch = line.match(/^Prompt user to\s+"([^"]*)"$/i);
333
+ if (promptMatch) {
334
+ flushCurrentStep();
335
+ workflow.steps.push({
336
+ type: 'prompt',
337
+ question: promptMatch[1],
338
+ stepNumber: workflow.steps.length + 1,
339
+ saveAs: null
340
+ });
146
341
  continue;
147
342
  }
148
-
149
- // Parse If/When conditions
150
- const ifMatch = line.match(/^(?:If|When)\s+(.+)$/i);
151
- if (ifMatch) {
152
- ifCondition = ifMatch[1].trim();
153
- inIfBlock = true;
154
- ifBody = [];
343
+
344
+ // Persist
345
+ const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]*)"$/i);
346
+ if (persistMatch) {
347
+ flushCurrentStep();
348
+ workflow.steps.push({
349
+ type: 'persist',
350
+ variable: persistMatch[1].trim(),
351
+ target: persistMatch[2],
352
+ stepNumber: workflow.steps.length + 1
353
+ });
155
354
  continue;
156
355
  }
157
-
158
- const endIfMatch = line.match(/^End(?:If)?$/i);
159
- if (endIfMatch && inIfBlock) {
160
- if (currentStep) {
161
- workflow.steps.push(currentStep);
162
- currentStep = null;
163
- }
164
-
356
+
357
+ // Emit
358
+ const emitMatch = line.match(/^Emit\s+"([^"]+)"\s+with\s+(.+)$/i);
359
+ if (emitMatch) {
360
+ flushCurrentStep();
165
361
  workflow.steps.push({
166
- type: 'if',
167
- condition: ifCondition,
168
- body: ifBody,
362
+ type: 'emit',
363
+ event: emitMatch[1],
364
+ payload: emitMatch[2].trim(),
169
365
  stepNumber: workflow.steps.length + 1
170
366
  });
171
-
172
- inIfBlock = false;
173
- ifCondition = null;
174
- ifBody = [];
175
367
  continue;
176
368
  }
177
-
178
- // Handle lines inside If block
179
- if (inIfBlock) {
180
- ifBody.push(line);
369
+
370
+ // Use (for Notify-like actions)
371
+ const useMatch = line.match(/^Use\s+(.+)$/i);
372
+ if (useMatch) {
373
+ flushCurrentStep();
374
+ workflow.steps.push({
375
+ type: 'use',
376
+ tool: useMatch[1].trim(),
377
+ stepNumber: workflow.steps.length + 1,
378
+ saveAs: null,
379
+ constraints: {}
380
+ });
181
381
  continue;
182
382
  }
183
-
184
- // Parse Return statement
383
+
384
+ // Ask (for Notify/resolver calls)
385
+ const askMatch = line.match(/^Ask\s+(.+)$/i);
386
+ if (askMatch) {
387
+ flushCurrentStep();
388
+ workflow.steps.push({
389
+ type: 'ask',
390
+ target: askMatch[1].trim(),
391
+ stepNumber: workflow.steps.length + 1,
392
+ saveAs: null,
393
+ constraints: {}
394
+ });
395
+ continue;
396
+ }
397
+
398
+ // Return
185
399
  const returnMatch = line.match(/^Return\s+(.+)$/i);
186
400
  if (returnMatch) {
187
- if (currentStep) {
188
- workflow.steps.push(currentStep);
189
- currentStep = null;
190
- }
401
+ flushCurrentStep();
191
402
  workflow.returnValues = returnMatch[1].split(',').map(r => r.trim()).filter(r => r !== '');
192
403
  continue;
193
404
  }
194
-
195
- // If we reach here and have unprocessed content, it's likely a workflow line without "Step X:"
196
- // Try to handle it as a step
405
+
406
+ // Fallback: treat as action
197
407
  if (line.trim() !== '') {
198
408
  if (!currentStep) {
199
409
  currentStep = {
@@ -204,51 +414,179 @@ function parseWorkflowLines(lines, filename) {
204
414
  constraints: {}
205
415
  };
206
416
  } else {
207
- // Append to current step action (multi-line)
208
417
  currentStep.actionRaw += ' ' + line;
209
418
  }
210
419
  }
211
420
  }
212
-
213
- // Don't forget the last step
214
- if (currentStep) {
215
- workflow.steps.push(currentStep);
216
- }
217
-
218
- // Post-process steps to extract Save as from actionRaw
421
+
422
+ flushCurrentStep(); // Final flush
423
+
424
+ // Post-process Save as in actionRaw - ✅ Apply normalization
219
425
  workflow.steps.forEach(step => {
220
426
  if (step.actionRaw && step.saveAs === null) {
221
427
  const saveInAction = step.actionRaw.match(/(.+?)\s+Save as\s+(.+)$/i);
222
428
  if (saveInAction) {
223
429
  step.actionRaw = saveInAction[1].trim();
224
- step.saveAs = saveInAction[2].trim();
430
+ step.saveAs = normalizeSymbol(saveInAction[2].trim());
225
431
  }
226
432
  }
433
+ // ✅ Also normalize any existing saveAs values
434
+ if (step.saveAs) {
435
+ step.saveAs = normalizeSymbol(step.saveAs);
436
+ }
227
437
  });
228
-
229
- // Check for common issues
438
+
439
+ // Validation warnings
230
440
  if (!workflow.name) {
231
441
  workflow.__warnings.push('Workflow name not found');
232
442
  }
233
-
234
443
  if (workflow.steps.length === 0) {
235
444
  workflow.__warnings.push('No steps found in workflow');
236
445
  }
237
-
238
446
  if (workflow.returnValues.length === 0 && workflow.steps.length > 0) {
239
447
  workflow.__warnings.push('No Return statement found');
240
448
  }
241
-
449
+
242
450
  return workflow;
243
451
  }
244
452
 
453
+ // Parses blocks (for parallel, if, escalation levels)
454
+ function parseBlock(lines) {
455
+ const steps = [];
456
+ let current = null;
457
+
458
+ const flush = () => {
459
+ if (current) {
460
+ steps.push(current);
461
+ current = null;
462
+ }
463
+ };
464
+
465
+ for (let line of lines) {
466
+ line = line.trim();
467
+ if (!line || line.startsWith('#')) continue;
468
+
469
+ const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
470
+ if (stepMatch) {
471
+ flush();
472
+ current = {
473
+ type: 'action',
474
+ stepNumber: parseInt(stepMatch[1], 10),
475
+ actionRaw: stepMatch[2].trim(),
476
+ saveAs: null,
477
+ constraints: {}
478
+ };
479
+ continue;
480
+ }
481
+
482
+ // Save as - ✅ Apply normalization
483
+ const saveMatch = line.match(/^Save as\s+(.+)$/i);
484
+ if (saveMatch && current) {
485
+ current.saveAs = normalizeSymbol(saveMatch[1].trim());
486
+ continue;
487
+ }
488
+
489
+ // Handle all special steps inside blocks
490
+ const debriefMatch = line.match(/^Debrief\s+([^\s]+)\s+with\s+"([^"]*)"$/i);
491
+ if (debriefMatch) {
492
+ flush();
493
+ steps.push({ type: 'debrief', agent: debriefMatch[1].trim(), message: debriefMatch[2] });
494
+ continue;
495
+ }
496
+
497
+ const evolveMatch = line.match(/^Evolve\s+([^\s]+)\s+using\s+feedback:\s*"([^"]*)"$/i);
498
+ if (evolveMatch) {
499
+ flush();
500
+ steps.push({ type: 'evolve', targetResolver: evolveMatch[1].trim(), feedback: evolveMatch[2] });
501
+ continue;
502
+ }
503
+
504
+ const promptMatch = line.match(/^Prompt user to\s+"([^"]*)"$/i);
505
+ if (promptMatch) {
506
+ flush();
507
+ steps.push({ type: 'prompt', question: promptMatch[1], saveAs: null });
508
+ continue;
509
+ }
510
+
511
+ const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]*)"$/i);
512
+ if (persistMatch) {
513
+ flush();
514
+ steps.push({ type: 'persist', variable: persistMatch[1].trim(), target: persistMatch[2] });
515
+ continue;
516
+ }
517
+
518
+ const emitMatch = line.match(/^Emit\s+"([^"]+)"\s+with\s+(.+)$/i);
519
+ if (emitMatch) {
520
+ flush();
521
+ steps.push({ type: 'emit', event: emitMatch[1], payload: emitMatch[2].trim() });
522
+ continue;
523
+ }
524
+
525
+ const useMatch = line.match(/^Use\s+(.+)$/i);
526
+ if (useMatch) {
527
+ flush();
528
+ steps.push({ type: 'use', tool: useMatch[1].trim(), saveAs: null, constraints: {} });
529
+ continue;
530
+ }
531
+
532
+ const askMatch = line.match(/^Ask\s+(.+)$/i);
533
+ if (askMatch) {
534
+ flush();
535
+ steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
536
+ continue;
537
+ }
538
+
539
+ // Constraint inside block
540
+ const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
541
+ if (constraintMatch && current) {
542
+ const constraintLine = constraintMatch[1].trim();
543
+ const eq = constraintLine.match(/^([^=]+)=\s*(.+)$/);
544
+ if (eq) {
545
+ let key = eq[1].trim();
546
+ let value = eq[2].trim();
547
+ if (value.startsWith('[') && value.endsWith(']')) {
548
+ value = value.slice(1, -1).split(',').map(v => v.trim().replace(/^"/, '').replace(/"$/, ''));
549
+ } else if (!isNaN(value)) {
550
+ value = Number(value);
551
+ } else if (value.startsWith('"') && value.endsWith('"')) {
552
+ value = value.slice(1, -1);
553
+ }
554
+ current.constraints[key] = value;
555
+ }
556
+ continue;
557
+ }
558
+
559
+ // Fallback
560
+ if (current) {
561
+ current.actionRaw += ' ' + line;
562
+ }
563
+ }
564
+
565
+ flush(); // ✅ Final flush in block
566
+
567
+ // ✅ Post-process Save as for steps inside blocks - Apply normalization
568
+ steps.forEach(step => {
569
+ if (step.actionRaw && step.saveAs === null) {
570
+ const saveInAction = step.actionRaw.match(/(.+?)\s+Save as\s+(.+)$/i);
571
+ if (saveInAction) {
572
+ step.actionRaw = saveInAction[1].trim();
573
+ step.saveAs = normalizeSymbol(saveInAction[2].trim());
574
+ }
575
+ }
576
+ // ✅ Also normalize any existing saveAs values
577
+ if (step.saveAs) {
578
+ step.saveAs = normalizeSymbol(step.saveAs);
579
+ }
580
+ });
581
+
582
+ return steps;
583
+ }
584
+
245
585
  function validate(workflow) {
246
586
  const errors = [];
247
-
248
587
  if (workflow.maxGenerations !== null && workflow.maxGenerations <= 0) {
249
588
  errors.push('max_generations must be positive');
250
589
  }
251
-
252
590
  return errors;
253
591
  }
254
592