@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/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
  // -----------------------------
@@ -145,6 +172,67 @@ class RuntimeAPI {
145
172
  }
146
173
  }
147
174
 
175
+ // -----------------------------
176
+ // ✅ ADDITION 1 — External Resolver Detection
177
+ // -----------------------------
178
+ _isExternalResolver(resolver) {
179
+ return Boolean(
180
+ resolver &&
181
+ resolver.manifest &&
182
+ typeof resolver.manifest === 'object' &&
183
+ typeof resolver.manifest.protocol === 'string' &&
184
+ resolver.manifest.protocol.startsWith('http')
185
+ );
186
+ }
187
+
188
+ // -----------------------------
189
+ // ✅ ADDITION 2 — External Resolver Invocation (HTTP Enforcement)
190
+ // -----------------------------
191
+ async _callExternalResolver(resolver, action, context) {
192
+ const manifest = resolver.manifest;
193
+ const endpoint = manifest.endpoint;
194
+ const timeoutMs = manifest.timeout_ms || 30000;
195
+
196
+ const payload = {
197
+ action,
198
+ context,
199
+ resolver: resolver.resolverName,
200
+ workflow: context.workflow_name,
201
+ timestamp: new Date().toISOString()
202
+ };
203
+
204
+ const controller = new AbortController();
205
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
206
+
207
+ try {
208
+ const res = await fetch(`${endpoint}/resolve`, {
209
+ method: 'POST',
210
+ headers: { 'Content-Type': 'application/json' },
211
+ body: JSON.stringify(payload),
212
+ signal: controller.signal
213
+ });
214
+
215
+ if (!res.ok) {
216
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
217
+ }
218
+
219
+ const json = await res.json();
220
+
221
+ if (json?.error) {
222
+ throw new Error(json.error.message || 'External resolver error');
223
+ }
224
+
225
+ return json.result;
226
+ } catch (err) {
227
+ if (err.name === 'AbortError') {
228
+ throw new Error(`External resolver timeout after ${timeoutMs}ms`);
229
+ }
230
+ throw err;
231
+ } finally {
232
+ clearTimeout(timer);
233
+ }
234
+ }
235
+
148
236
  // -----------------------------
149
237
  // Utilities
150
238
  // -----------------------------
@@ -218,15 +306,40 @@ class RuntimeAPI {
218
306
  async executeStep(step, agentResolver) {
219
307
  const stepType = step.type;
220
308
 
221
- const validateResolver = (resolver) => {
222
- const resolverName = (resolver?.resolverName || resolver?.name || '').trim();
223
- if (!resolverName) throw new Error('[O-Lang] Resolver missing name metadata');
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
+
321
+ // ✅ ADDITION 3 — Resolver Policy Enforcement (External + Local)
322
+ const enforceResolverPolicy = (resolver, step) => {
323
+ const resolverName = resolver?.resolverName || resolver?.name;
224
324
 
225
- const allowed = Array.from(this.allowedResolvers || []).map(r => r.trim());
325
+ if (!resolverName) {
326
+ throw new Error('[O-Lang] Resolver missing resolverName');
327
+ }
226
328
 
227
- if (!allowed.includes(resolverName)) {
228
- this.logDisallowedResolver(resolverName, step.actionRaw || step.tool || step.target);
229
- throw new Error(`[O-Lang] Resolver "${resolverName}" is not allowed by workflow policy`);
329
+ if (!this.allowedResolvers.has(resolverName)) {
330
+ this.logDisallowedResolver(resolverName, step.actionRaw || step.type);
331
+ throw new Error(
332
+ `[O-Lang] Resolver "${resolverName}" blocked by workflow policy`
333
+ );
334
+ }
335
+
336
+ // External resolvers MUST be HTTP-only
337
+ if (this._isExternalResolver(resolver)) {
338
+ if (!resolver.manifest.endpoint) {
339
+ throw new Error(
340
+ `[O-Lang] External resolver "${resolverName}" missing endpoint`
341
+ );
342
+ }
230
343
  }
231
344
  };
232
345
 
@@ -261,17 +374,28 @@ class RuntimeAPI {
261
374
  // ✅ Return the FIRST resolver that returns a non-undefined result
262
375
  for (let idx = 0; idx < resolversToRun.length; idx++) {
263
376
  const resolver = resolversToRun[idx];
264
- validateResolver(resolver);
377
+ enforceResolverPolicy(resolver, step); // ✅ Use new policy enforcement
265
378
 
266
379
  try {
267
- const out = await resolver(action, this.context);
268
- outputs.push(out);
269
- this.context[`__resolver_${idx}`] = out;
270
-
271
- // ✅ If resolver handled the action (returned non-undefined), use it immediately
272
- if (out !== undefined) {
273
- return out;
380
+ let result; // ADDITION 4 — External Resolver Execution Path
381
+
382
+ if (this._isExternalResolver(resolver)) {
383
+ result = await this._callExternalResolver(
384
+ resolver,
385
+ action,
386
+ this.context
387
+ );
388
+ } else {
389
+ result = await resolver(action, this.context);
390
+ }
391
+
392
+ if (result !== undefined) {
393
+ this.context[`__resolver_${idx}`] = result;
394
+ return result;
274
395
  }
396
+
397
+ outputs.push(result);
398
+ this.context[`__resolver_${idx}`] = result;
275
399
  } catch (e) {
276
400
  this.addWarning(`Resolver ${resolver?.resolverName || resolver?.name || idx} failed for action "${action}": ${e.message}`);
277
401
  outputs.push(null);
@@ -329,14 +453,12 @@ class RuntimeAPI {
329
453
  }
330
454
 
331
455
  case 'evolve': {
332
- // ✅ Handle in-workflow Evolve steps
333
456
  const { targetResolver, feedback } = step;
334
457
 
335
458
  if (this.verbose) {
336
459
  console.log(`🔄 Evolve step: ${targetResolver} with feedback: "${feedback}"`);
337
460
  }
338
461
 
339
- // Basic evolution: record the request (free tier)
340
462
  const evolutionResult = {
341
463
  resolver: targetResolver,
342
464
  feedback: feedback,
@@ -345,12 +467,10 @@ class RuntimeAPI {
345
467
  workflow: this.context.workflow_name
346
468
  };
347
469
 
348
- // ✅ Check for Advanced Evolution Service (paid tier)
349
- // Check for Advanced Evolution Service (paid tier)
350
- if (process.env.OLANG_EVOLUTION_API_KEY) {
351
- evolutionResult.status = 'advanced_evolution_enabled';
352
- evolutionResult.message = 'Advanced evolution service would process this request';
353
- }
470
+ if (process.env.OLANG_EVOLUTION_API_KEY) {
471
+ evolutionResult.status = 'advanced_evolution_enabled';
472
+ evolutionResult.message = 'Advanced evolution service would process this request';
473
+ }
354
474
 
355
475
  if (step.saveAs) {
356
476
  this.context[step.saveAs] = evolutionResult;
@@ -366,7 +486,99 @@ class RuntimeAPI {
366
486
  }
367
487
 
368
488
  case 'parallel': {
369
- await Promise.all(step.steps.map(s => this.executeStep(s, agentResolver)));
489
+ const { steps, timeout } = step;
490
+
491
+ if (timeout !== undefined && timeout > 0) {
492
+ // Timed parallel execution
493
+ const timeoutPromise = new Promise(resolve => {
494
+ setTimeout(() => resolve({ timedOut: true }), timeout);
495
+ });
496
+
497
+ const parallelPromise = Promise.all(
498
+ steps.map(s => this.executeStep(s, agentResolver))
499
+ ).then(() => ({ timedOut: false }));
500
+
501
+ const result = await Promise.race([timeoutPromise, parallelPromise]);
502
+ this.context.timed_out = result.timedOut;
503
+
504
+ if (result.timedOut) {
505
+ this.emit('parallel_timeout', { duration: timeout, steps: steps.length });
506
+ if (this.verbose) {
507
+ console.log(`⏰ Parallel execution timed out after ${timeout}ms`);
508
+ }
509
+ }
510
+ } else {
511
+ // Normal parallel execution (no timeout)
512
+ await Promise.all(steps.map(s => this.executeStep(s, agentResolver)));
513
+ this.context.timed_out = false;
514
+ }
515
+ break;
516
+ }
517
+
518
+ case 'escalation': {
519
+ const { levels } = step;
520
+ let finalResult = null;
521
+ let currentTimeout = 0;
522
+ let completedLevel = null;
523
+
524
+ for (const level of levels) {
525
+ if (level.timeout === 0) {
526
+ // Immediate execution (no timeout)
527
+ const levelSteps = require('./parser').parseBlock(level.steps);
528
+ for (const levelStep of levelSteps) {
529
+ await this.executeStep(levelStep, agentResolver);
530
+ }
531
+
532
+ // Check if the target variable was set in this level
533
+ // For now, we'll assume the last saveAs in the level is the result
534
+ if (levelSteps.length > 0) {
535
+ const lastStep = levelSteps[levelSteps.length - 1];
536
+ if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
537
+ finalResult = this.context[lastStep.saveAs];
538
+ completedLevel = level.levelNumber;
539
+ break;
540
+ }
541
+ }
542
+ } else {
543
+ // Timed execution for this level
544
+ currentTimeout += level.timeout;
545
+
546
+ const timeoutPromise = new Promise(resolve => {
547
+ setTimeout(() => resolve({ timedOut: true }), level.timeout);
548
+ });
549
+
550
+ const levelPromise = (async () => {
551
+ const levelSteps = require('./parser').parseBlock(level.steps);
552
+ for (const levelStep of levelSteps) {
553
+ await this.executeStep(levelStep, agentResolver);
554
+ }
555
+ return { timedOut: false };
556
+ })();
557
+
558
+ const result = await Promise.race([timeoutPromise, levelPromise]);
559
+
560
+ if (!result.timedOut) {
561
+ // Level completed successfully
562
+ if (levelSteps && levelSteps.length > 0) {
563
+ const lastStep = levelSteps[levelSteps.length - 1];
564
+ if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
565
+ finalResult = this.context[lastStep.saveAs];
566
+ completedLevel = level.levelNumber;
567
+ break;
568
+ }
569
+ }
570
+ }
571
+ // If timed out, continue to next level
572
+ }
573
+ }
574
+
575
+ // Set escalation status in context
576
+ this.context.escalation_completed = finalResult !== null;
577
+ this.context.timed_out = finalResult === null;
578
+ if (completedLevel !== null) {
579
+ this.context.escalation_level = completedLevel;
580
+ }
581
+
370
582
  break;
371
583
  }
372
584
 
@@ -381,26 +593,87 @@ class RuntimeAPI {
381
593
  }
382
594
 
383
595
  case 'debrief': {
596
+ // ✅ SEMANTIC VALIDATION: Check symbols in message
597
+ if (step.message.includes('{')) {
598
+ const symbols = step.message.match(/\{([^\}]+)\}/g) || [];
599
+ for (const symbolMatch of symbols) {
600
+ const symbol = symbolMatch.replace(/[{}]/g, '');
601
+ this._requireSemantic(symbol, 'debrief');
602
+ }
603
+ }
384
604
  this.emit('debrief', { agent: step.agent, message: step.message });
385
605
  break;
386
606
  }
387
607
 
388
- // ✅ File Persist step handler
389
- case 'persist': {
390
- const sourceValue = this.getNested(this.context, step.source);
391
- if (sourceValue === undefined) {
392
- this.addWarning(`Cannot persist undefined value from "${step.source}" to "${step.destination}"`);
608
+ // ✅ NEW: Prompt step handler
609
+ case 'prompt': {
610
+ if (this.verbose) {
611
+ console.log(`❓ Prompt: ${step.question}`);
612
+ }
613
+ // In non-interactive mode, leave as no-op
614
+ // (Could integrate with stdin or API in future)
615
+ break;
616
+ }
617
+
618
+ // ✅ NEW: Emit step handler with semantic validation
619
+ case 'emit': {
620
+ // ✅ SEMANTIC VALIDATION: Check all symbols in payload
621
+ const payloadTemplate = step.payload;
622
+ const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
623
+
624
+ let shouldEmit = true;
625
+ for (const symbolMatch of symbols) {
626
+ const symbol = symbolMatch.replace(/[{}]/g, '');
627
+ if (!this._requireSemantic(symbol, 'emit')) {
628
+ shouldEmit = false;
629
+ // Continue to validate all symbols (for complete error reporting)
630
+ }
631
+ }
632
+
633
+ if (!shouldEmit) {
634
+ if (this.verbose) {
635
+ console.log(`⏭️ Skipped emit due to missing semantic symbols`);
636
+ }
393
637
  break;
394
638
  }
639
+
640
+ const payload = step.payload.replace(/\{([^\}]+)\}/g, (_, path) => {
641
+ const value = this.getNested(this.context, path.trim());
642
+ return value !== undefined ? String(value) : `{${path}}`;
643
+ });
644
+
645
+ this.emit(step.event, {
646
+ payload: payload,
647
+ workflow: this.context.workflow_name,
648
+ timestamp: new Date().toISOString()
649
+ });
650
+
651
+ if (this.verbose) {
652
+ console.log(`📤 Emit event "${step.event}" with payload: ${payload}`);
653
+ }
654
+ break;
655
+ }
395
656
 
396
- const outputPath = path.resolve(process.cwd(), step.destination);
657
+ // File Persist step handler with semantic validation
658
+ case 'persist': {
659
+ // ✅ SEMANTIC VALIDATION: Require symbol exists
660
+ if (!this._requireSemantic(step.variable, 'persist')) {
661
+ // Default policy: Skip persist (safe)
662
+ if (this.verbose) {
663
+ console.log(`⏭️ Skipped persist for undefined "${step.variable}"`);
664
+ }
665
+ break;
666
+ }
667
+
668
+ const sourceValue = this.context[step.variable]; // Now guaranteed defined
669
+ const outputPath = path.resolve(process.cwd(), step.target);
397
670
  const outputDir = path.dirname(outputPath);
398
671
  if (!fs.existsSync(outputDir)) {
399
672
  fs.mkdirSync(outputDir, { recursive: true });
400
673
  }
401
674
 
402
675
  let content;
403
- if (step.destination.endsWith('.json')) {
676
+ if (step.target.endsWith('.json')) {
404
677
  content = JSON.stringify(sourceValue, null, 2);
405
678
  } else {
406
679
  content = String(sourceValue);
@@ -409,23 +682,27 @@ class RuntimeAPI {
409
682
  fs.writeFileSync(outputPath, content, 'utf8');
410
683
 
411
684
  if (this.verbose) {
412
- console.log(`💾 Persisted "${step.source}" to ${step.destination}`);
685
+ console.log(`💾 Persisted "${step.variable}" to ${step.target}`);
413
686
  }
414
687
  break;
415
688
  }
416
689
 
417
- // ✅ NEW: Database persist handler
690
+ // ✅ NEW: Database persist handler with semantic validation
418
691
  case 'persist-db': {
419
692
  if (!this.dbClient) {
420
693
  this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
421
694
  break;
422
695
  }
423
696
 
424
- const sourceValue = this.getNested(this.context, step.source);
425
- if (sourceValue === undefined) {
426
- this.addWarning(`Cannot persist undefined value from "${step.source}" to DB collection "${step.collection}"`);
697
+ // SEMANTIC VALIDATION: Require symbol exists
698
+ if (!this._requireSemantic(step.variable, 'persist-db')) {
699
+ if (this.verbose) {
700
+ console.log(`⏭️ Skipped DB persist for undefined "${step.variable}"`);
701
+ }
427
702
  break;
428
703
  }
704
+
705
+ const sourceValue = this.context[step.variable]; // Now guaranteed defined
429
706
 
430
707
  try {
431
708
  switch (this.dbClient.type) {
@@ -466,10 +743,10 @@ class RuntimeAPI {
466
743
  }
467
744
 
468
745
  if (this.verbose) {
469
- console.log(`🗄️ Persisted "${step.source}" to DB collection ${step.collection}`);
746
+ console.log(`🗄️ Persisted "${step.variable}" to DB collection ${step.collection}`);
470
747
  }
471
748
  } catch (e) {
472
- this.addWarning(`DB persist failed for "${step.source}": ${e.message}`);
749
+ this.addWarning(`DB persist failed for "${step.variable}": ${e.message}`);
473
750
  }
474
751
  break;
475
752
  }
@@ -482,18 +759,15 @@ class RuntimeAPI {
482
759
  }
483
760
 
484
761
  async executeWorkflow(workflow, inputs, agentResolver) {
485
- // Handle regular workflows only (Evolve is a step type now)
486
762
  if (workflow.type !== 'workflow') {
487
763
  throw new Error(`Unknown workflow type: ${workflow.type}`);
488
764
  }
489
765
 
490
- // ✅ Inject workflow name into context
491
766
  this.context = {
492
767
  ...inputs,
493
768
  workflow_name: workflow.name
494
769
  };
495
770
 
496
- // ✅ Check generation constraint from Constraint: max_generations = X
497
771
  const currentGeneration = inputs.__generation || 1;
498
772
  if (workflow.maxGenerations !== null && currentGeneration > workflow.maxGenerations) {
499
773
  throw new Error(`Workflow generation ${currentGeneration} exceeds Constraint: max_generations = ${workflow.maxGenerations}`);
@@ -524,9 +798,13 @@ class RuntimeAPI {
524
798
  });
525
799
  }
526
800
 
801
+ // ✅ SEMANTIC VALIDATION: For return values
527
802
  const result = {};
528
803
  for (const key of workflow.returnValues) {
529
- result[key] = this.getNested(this.context, key);
804
+ if (this._requireSemantic(key, 'return')) {
805
+ result[key] = this.context[key];
806
+ }
807
+ // Skip undefined return values (safe default)
530
808
  }
531
809
  return result;
532
810
  }