@o-lang/olang 1.0.27 → 1.1.2

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/cli.js CHANGED
@@ -1,10 +1,13 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  const { Command } = require('commander');
3
3
  const { parse } = require('./src/parser');
4
4
  const { execute } = require('./src/runtime');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
 
8
+ // === ADDED: Load package.json for version (1 line added) ===
9
+ const pkg = require('./package.json');
10
+
8
11
  /**
9
12
  * Enforce .ol extension ONLY (CLI only)
10
13
  */
@@ -156,6 +159,9 @@ function loadResolverChain(specifiers, verbose, allowed) {
156
159
  */
157
160
  const program = new Command();
158
161
 
162
+ // === ADDED: Version support (1 line added) ===
163
+ program.version(pkg.version, '-V, --version', 'Show O-lang kernel version');
164
+
159
165
  // === RUN COMMAND ===
160
166
  program
161
167
  .name('olang')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.0.27",
3
+ "version": "1.1.2",
4
4
  "author": "Olalekan Ogundipe <info@workfily.com>",
5
5
  "description": "O-Lang: A governance language for user-directed, rule-enforced agent workflows",
6
6
  "main": "./src/index.js",
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
 
package/src/runtime.js CHANGED
@@ -92,6 +92,33 @@ class RuntimeAPI {
92
92
  }
93
93
  }
94
94
 
95
+ // -----------------------------
96
+ // ✅ SEMANTIC ENFORCEMENT HELPER
97
+ // -----------------------------
98
+ _requireSemantic(symbol, stepType) {
99
+ const value = this.context[symbol];
100
+ if (value === undefined) {
101
+ const error = {
102
+ type: 'semantic_violation',
103
+ symbol: symbol,
104
+ expected: 'defined value',
105
+ used_by: stepType,
106
+ phase: 'execution'
107
+ };
108
+
109
+ // Emit semantic event (for observability)
110
+ this.emit('semantic_violation', error);
111
+
112
+ // Log as error (not warning)
113
+ if (this.verbose) {
114
+ console.error(`[O-Lang SEMANTIC] Missing required symbol "${symbol}" for ${stepType}`);
115
+ }
116
+
117
+ return false;
118
+ }
119
+ return true;
120
+ }
121
+
95
122
  // -----------------------------
96
123
  // Parser/runtime warnings
97
124
  // -----------------------------
@@ -148,11 +175,6 @@ class RuntimeAPI {
148
175
  // -----------------------------
149
176
  // ✅ ADDITION 1 — External Resolver Detection
150
177
  // -----------------------------
151
- /**
152
- * Determines whether a resolver is external (HTTP-based)
153
- * External resolvers MUST be declared via a manifest (.json)
154
- * and explicitly allowed by workflow policy
155
- */
156
178
  _isExternalResolver(resolver) {
157
179
  return Boolean(
158
180
  resolver &&
@@ -166,13 +188,6 @@ class RuntimeAPI {
166
188
  // -----------------------------
167
189
  // ✅ ADDITION 2 — External Resolver Invocation (HTTP Enforcement)
168
190
  // -----------------------------
169
- /**
170
- * Calls an external HTTP resolver using its manifest definition.
171
- * Enforces:
172
- * - timeout
173
- * - JSON contract
174
- * - isolation (no direct execution)
175
- */
176
191
  async _callExternalResolver(resolver, action, context) {
177
192
  const manifest = resolver.manifest;
178
193
  const endpoint = manifest.endpoint;
@@ -291,6 +306,18 @@ class RuntimeAPI {
291
306
  async executeStep(step, agentResolver) {
292
307
  const stepType = step.type;
293
308
 
309
+ // ✅ Enforce per-step constraints (basic validation)
310
+ if (step.constraints && Object.keys(step.constraints).length > 0) {
311
+ for (const [key, value] of Object.entries(step.constraints)) {
312
+ // Log unsupported constraints (future extensibility)
313
+ if (['max_time_sec', 'cost_limit', 'allowed_resolvers'].includes(key)) {
314
+ this.addWarning(`Per-step constraint "${key}=${value}" is parsed but not yet enforced`);
315
+ } else {
316
+ this.addWarning(`Unknown per-step constraint: ${key}=${value}`);
317
+ }
318
+ }
319
+ }
320
+
294
321
  // ✅ ADDITION 3 — Resolver Policy Enforcement (External + Local)
295
322
  const enforceResolverPolicy = (resolver, step) => {
296
323
  const resolverName = resolver?.resolverName || resolver?.name;
@@ -316,9 +343,8 @@ class RuntimeAPI {
316
343
  }
317
344
  };
318
345
 
346
+ // ✅ STRICT SAFETY: Updated runResolvers with failure halting
319
347
  const runResolvers = async (action) => {
320
- const outputs = [];
321
-
322
348
  const mathPattern =
323
349
  /^(Add|Subtract|Multiply|Divide|Sum|Avg|Min|Max|Round|Floor|Ceil|Abs)\b/i;
324
350
 
@@ -344,13 +370,13 @@ class RuntimeAPI {
344
370
  resolversToRun = [agentResolver];
345
371
  }
346
372
 
347
- // ✅ Return the FIRST resolver that returns a non-undefined result
373
+ // ✅ STRICT SAFETY: Fail fast on resolver errors or empty results
348
374
  for (let idx = 0; idx < resolversToRun.length; idx++) {
349
375
  const resolver = resolversToRun[idx];
350
- enforceResolverPolicy(resolver, step); // ✅ Use new policy enforcement
376
+ enforceResolverPolicy(resolver, step);
351
377
 
352
378
  try {
353
- let result; // ✅ ADDITION 4 — External Resolver Execution Path
379
+ let result;
354
380
 
355
381
  if (this._isExternalResolver(resolver)) {
356
382
  result = await this._callExternalResolver(
@@ -362,22 +388,32 @@ class RuntimeAPI {
362
388
  result = await resolver(action, this.context);
363
389
  }
364
390
 
365
- if (result !== undefined) {
366
- this.context[`__resolver_${idx}`] = result;
367
- return result;
391
+ // SAFETY GUARD 1: Reject undefined/null results
392
+ if (result === undefined || result === null) {
393
+ throw new Error(
394
+ `[O-Lang SAFETY] Resolver "${resolver.resolverName || resolver.name || 'anonymous'}" returned empty result for action: "${action}". ` +
395
+ `Workflow halted to prevent unsafe data propagation.`
396
+ );
368
397
  }
369
398
 
370
- outputs.push(result);
371
399
  this.context[`__resolver_${idx}`] = result;
400
+ return result;
401
+
372
402
  } catch (e) {
373
- this.addWarning(`Resolver ${resolver?.resolverName || resolver?.name || idx} failed for action "${action}": ${e.message}`);
374
- outputs.push(null);
375
- this.context[`__resolver_${idx}`] = null;
403
+ // SAFETY GUARD 2: HALT workflow immediately on resolver failure
404
+ throw new Error(
405
+ `[O-Lang SAFETY] Resolver "${resolver?.resolverName || resolver?.name || idx}" failed for action "${action}": ${e.message}\n` +
406
+ `Workflow execution halted.`
407
+ );
376
408
  }
377
409
  }
378
410
 
379
- // If no resolver handled the action, return undefined
380
- return undefined;
411
+ // SAFETY GUARD 3: No resolver handled the action HALT
412
+ throw new Error(
413
+ `[O-Lang SAFETY] No resolver handled action: "${action}". ` +
414
+ `Available resolvers: ${resolversToRun.map(r => r.resolverName || r.name || 'anonymous').join(', ')}. ` +
415
+ `Workflow execution halted.`
416
+ );
381
417
  };
382
418
 
383
419
  switch (stepType) {
@@ -426,14 +462,12 @@ class RuntimeAPI {
426
462
  }
427
463
 
428
464
  case 'evolve': {
429
- // ✅ Handle in-workflow Evolve steps
430
465
  const { targetResolver, feedback } = step;
431
466
 
432
467
  if (this.verbose) {
433
468
  console.log(`🔄 Evolve step: ${targetResolver} with feedback: "${feedback}"`);
434
469
  }
435
470
 
436
- // Basic evolution: record the request (free tier)
437
471
  const evolutionResult = {
438
472
  resolver: targetResolver,
439
473
  feedback: feedback,
@@ -442,12 +476,10 @@ class RuntimeAPI {
442
476
  workflow: this.context.workflow_name
443
477
  };
444
478
 
445
- // ✅ Check for Advanced Evolution Service (paid tier)
446
- // Check for Advanced Evolution Service (paid tier)
447
- if (process.env.OLANG_EVOLUTION_API_KEY) {
448
- evolutionResult.status = 'advanced_evolution_enabled';
449
- evolutionResult.message = 'Advanced evolution service would process this request';
450
- }
479
+ if (process.env.OLANG_EVOLUTION_API_KEY) {
480
+ evolutionResult.status = 'advanced_evolution_enabled';
481
+ evolutionResult.message = 'Advanced evolution service would process this request';
482
+ }
451
483
 
452
484
  if (step.saveAs) {
453
485
  this.context[step.saveAs] = evolutionResult;
@@ -463,7 +495,99 @@ class RuntimeAPI {
463
495
  }
464
496
 
465
497
  case 'parallel': {
466
- await Promise.all(step.steps.map(s => this.executeStep(s, agentResolver)));
498
+ const { steps, timeout } = step;
499
+
500
+ if (timeout !== undefined && timeout > 0) {
501
+ // Timed parallel execution
502
+ const timeoutPromise = new Promise(resolve => {
503
+ setTimeout(() => resolve({ timedOut: true }), timeout);
504
+ });
505
+
506
+ const parallelPromise = Promise.all(
507
+ steps.map(s => this.executeStep(s, agentResolver))
508
+ ).then(() => ({ timedOut: false }));
509
+
510
+ const result = await Promise.race([timeoutPromise, parallelPromise]);
511
+ this.context.timed_out = result.timedOut;
512
+
513
+ if (result.timedOut) {
514
+ this.emit('parallel_timeout', { duration: timeout, steps: steps.length });
515
+ if (this.verbose) {
516
+ console.log(`⏰ Parallel execution timed out after ${timeout}ms`);
517
+ }
518
+ }
519
+ } else {
520
+ // Normal parallel execution (no timeout)
521
+ await Promise.all(steps.map(s => this.executeStep(s, agentResolver)));
522
+ this.context.timed_out = false;
523
+ }
524
+ break;
525
+ }
526
+
527
+ case 'escalation': {
528
+ const { levels } = step;
529
+ let finalResult = null;
530
+ let currentTimeout = 0;
531
+ let completedLevel = null;
532
+
533
+ for (const level of levels) {
534
+ if (level.timeout === 0) {
535
+ // Immediate execution (no timeout)
536
+ const levelSteps = require('./parser').parseBlock(level.steps);
537
+ for (const levelStep of levelSteps) {
538
+ await this.executeStep(levelStep, agentResolver);
539
+ }
540
+
541
+ // Check if the target variable was set in this level
542
+ // For now, we'll assume the last saveAs in the level is the result
543
+ if (levelSteps.length > 0) {
544
+ const lastStep = levelSteps[levelSteps.length - 1];
545
+ if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
546
+ finalResult = this.context[lastStep.saveAs];
547
+ completedLevel = level.levelNumber;
548
+ break;
549
+ }
550
+ }
551
+ } else {
552
+ // Timed execution for this level
553
+ currentTimeout += level.timeout;
554
+
555
+ const timeoutPromise = new Promise(resolve => {
556
+ setTimeout(() => resolve({ timedOut: true }), level.timeout);
557
+ });
558
+
559
+ const levelPromise = (async () => {
560
+ const levelSteps = require('./parser').parseBlock(level.steps);
561
+ for (const levelStep of levelSteps) {
562
+ await this.executeStep(levelStep, agentResolver);
563
+ }
564
+ return { timedOut: false };
565
+ })();
566
+
567
+ const result = await Promise.race([timeoutPromise, levelPromise]);
568
+
569
+ if (!result.timedOut) {
570
+ // Level completed successfully
571
+ if (levelSteps && levelSteps.length > 0) {
572
+ const lastStep = levelSteps[levelSteps.length - 1];
573
+ if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
574
+ finalResult = this.context[lastStep.saveAs];
575
+ completedLevel = level.levelNumber;
576
+ break;
577
+ }
578
+ }
579
+ }
580
+ // If timed out, continue to next level
581
+ }
582
+ }
583
+
584
+ // Set escalation status in context
585
+ this.context.escalation_completed = finalResult !== null;
586
+ this.context.timed_out = finalResult === null;
587
+ if (completedLevel !== null) {
588
+ this.context.escalation_level = completedLevel;
589
+ }
590
+
467
591
  break;
468
592
  }
469
593
 
@@ -478,26 +602,87 @@ class RuntimeAPI {
478
602
  }
479
603
 
480
604
  case 'debrief': {
605
+ // ✅ SEMANTIC VALIDATION: Check symbols in message
606
+ if (step.message.includes('{')) {
607
+ const symbols = step.message.match(/\{([^\}]+)\}/g) || [];
608
+ for (const symbolMatch of symbols) {
609
+ const symbol = symbolMatch.replace(/[{}]/g, '');
610
+ this._requireSemantic(symbol, 'debrief');
611
+ }
612
+ }
481
613
  this.emit('debrief', { agent: step.agent, message: step.message });
482
614
  break;
483
615
  }
484
616
 
485
- // ✅ File Persist step handler
486
- case 'persist': {
487
- const sourceValue = this.getNested(this.context, step.source);
488
- if (sourceValue === undefined) {
489
- this.addWarning(`Cannot persist undefined value from "${step.source}" to "${step.destination}"`);
617
+ // ✅ NEW: Prompt step handler
618
+ case 'prompt': {
619
+ if (this.verbose) {
620
+ console.log(`❓ Prompt: ${step.question}`);
621
+ }
622
+ // In non-interactive mode, leave as no-op
623
+ // (Could integrate with stdin or API in future)
624
+ break;
625
+ }
626
+
627
+ // ✅ NEW: Emit step handler with semantic validation
628
+ case 'emit': {
629
+ // ✅ SEMANTIC VALIDATION: Check all symbols in payload
630
+ const payloadTemplate = step.payload;
631
+ const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
632
+
633
+ let shouldEmit = true;
634
+ for (const symbolMatch of symbols) {
635
+ const symbol = symbolMatch.replace(/[{}]/g, '');
636
+ if (!this._requireSemantic(symbol, 'emit')) {
637
+ shouldEmit = false;
638
+ // Continue to validate all symbols (for complete error reporting)
639
+ }
640
+ }
641
+
642
+ if (!shouldEmit) {
643
+ if (this.verbose) {
644
+ console.log(`⏭️ Skipped emit due to missing semantic symbols`);
645
+ }
490
646
  break;
491
647
  }
648
+
649
+ const payload = step.payload.replace(/\{([^\}]+)\}/g, (_, path) => {
650
+ const value = this.getNested(this.context, path.trim());
651
+ return value !== undefined ? String(value) : `{${path}}`;
652
+ });
653
+
654
+ this.emit(step.event, {
655
+ payload: payload,
656
+ workflow: this.context.workflow_name,
657
+ timestamp: new Date().toISOString()
658
+ });
659
+
660
+ if (this.verbose) {
661
+ console.log(`📤 Emit event "${step.event}" with payload: ${payload}`);
662
+ }
663
+ break;
664
+ }
492
665
 
493
- const outputPath = path.resolve(process.cwd(), step.destination);
666
+ // File Persist step handler with semantic validation
667
+ case 'persist': {
668
+ // ✅ SEMANTIC VALIDATION: Require symbol exists
669
+ if (!this._requireSemantic(step.variable, 'persist')) {
670
+ // Default policy: Skip persist (safe)
671
+ if (this.verbose) {
672
+ console.log(`⏭️ Skipped persist for undefined "${step.variable}"`);
673
+ }
674
+ break;
675
+ }
676
+
677
+ const sourceValue = this.context[step.variable]; // Now guaranteed defined
678
+ const outputPath = path.resolve(process.cwd(), step.target);
494
679
  const outputDir = path.dirname(outputPath);
495
680
  if (!fs.existsSync(outputDir)) {
496
681
  fs.mkdirSync(outputDir, { recursive: true });
497
682
  }
498
683
 
499
684
  let content;
500
- if (step.destination.endsWith('.json')) {
685
+ if (step.target.endsWith('.json')) {
501
686
  content = JSON.stringify(sourceValue, null, 2);
502
687
  } else {
503
688
  content = String(sourceValue);
@@ -506,23 +691,27 @@ class RuntimeAPI {
506
691
  fs.writeFileSync(outputPath, content, 'utf8');
507
692
 
508
693
  if (this.verbose) {
509
- console.log(`💾 Persisted "${step.source}" to ${step.destination}`);
694
+ console.log(`💾 Persisted "${step.variable}" to ${step.target}`);
510
695
  }
511
696
  break;
512
697
  }
513
698
 
514
- // ✅ NEW: Database persist handler
699
+ // ✅ NEW: Database persist handler with semantic validation
515
700
  case 'persist-db': {
516
701
  if (!this.dbClient) {
517
702
  this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
518
703
  break;
519
704
  }
520
705
 
521
- const sourceValue = this.getNested(this.context, step.source);
522
- if (sourceValue === undefined) {
523
- this.addWarning(`Cannot persist undefined value from "${step.source}" to DB collection "${step.collection}"`);
706
+ // SEMANTIC VALIDATION: Require symbol exists
707
+ if (!this._requireSemantic(step.variable, 'persist-db')) {
708
+ if (this.verbose) {
709
+ console.log(`⏭️ Skipped DB persist for undefined "${step.variable}"`);
710
+ }
524
711
  break;
525
712
  }
713
+
714
+ const sourceValue = this.context[step.variable]; // Now guaranteed defined
526
715
 
527
716
  try {
528
717
  switch (this.dbClient.type) {
@@ -563,39 +752,31 @@ class RuntimeAPI {
563
752
  }
564
753
 
565
754
  if (this.verbose) {
566
- console.log(`🗄️ Persisted "${step.source}" to DB collection ${step.collection}`);
755
+ console.log(`🗄️ Persisted "${step.variable}" to DB collection ${step.collection}`);
567
756
  }
568
757
  } catch (e) {
569
- this.addWarning(`DB persist failed for "${step.source}": ${e.message}`);
758
+ this.addWarning(`DB persist failed for "${step.variable}": ${e.message}`);
570
759
  }
571
760
  break;
572
761
  }
573
762
  }
574
763
 
575
- // ✅ ADDITION 5 — Security Warning for External Resolvers
576
764
  if (this.verbose) {
577
- for (const r of this.allowedResolvers) {
578
- // Note: We can't easily check if resolvers are external here since we only have names
579
- // This would need to be moved to where we have the actual resolver objects
580
- }
581
765
  console.log(`\n[Step: ${step.type} | saveAs: ${step.saveAs || 'N/A'}]`);
582
766
  console.log(JSON.stringify(this.context, null, 2));
583
767
  }
584
768
  }
585
769
 
586
770
  async executeWorkflow(workflow, inputs, agentResolver) {
587
- // Handle regular workflows only (Evolve is a step type now)
588
771
  if (workflow.type !== 'workflow') {
589
772
  throw new Error(`Unknown workflow type: ${workflow.type}`);
590
773
  }
591
774
 
592
- // ✅ Inject workflow name into context
593
775
  this.context = {
594
776
  ...inputs,
595
777
  workflow_name: workflow.name
596
778
  };
597
779
 
598
- // ✅ Check generation constraint from Constraint: max_generations = X
599
780
  const currentGeneration = inputs.__generation || 1;
600
781
  if (workflow.maxGenerations !== null && currentGeneration > workflow.maxGenerations) {
601
782
  throw new Error(`Workflow generation ${currentGeneration} exceeds Constraint: max_generations = ${workflow.maxGenerations}`);
@@ -626,9 +807,13 @@ class RuntimeAPI {
626
807
  });
627
808
  }
628
809
 
810
+ // ✅ SEMANTIC VALIDATION: For return values
629
811
  const result = {};
630
812
  for (const key of workflow.returnValues) {
631
- result[key] = this.getNested(this.context, key);
813
+ if (this._requireSemantic(key, 'return')) {
814
+ result[key] = this.context[key];
815
+ }
816
+ // Skip undefined return values (safe default)
632
817
  }
633
818
  return result;
634
819
  }