@o-lang/olang 1.1.8 → 1.2.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.
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  const { Command } = require('commander');
3
- const { parse } = require('./src/parser');
4
- const { execute } = require('./src/runtime');
4
+ const { parse } = require('../src/parser');
5
+ const { execute } = require('../src/runtime');
5
6
  const fs = require('fs');
6
7
  const path = require('path');
8
+ const { createRequire } = require('module');
7
9
 
8
- // === ADDED: Load package.json for version (1 line added) ===
9
- const pkg = require('./package.json');
10
+ // === Load kernel package.json for version ===
11
+ const pkg = require('../package.json');
10
12
 
11
13
  /**
12
14
  * Enforce .ol extension ONLY (CLI only)
@@ -21,21 +23,21 @@ function ensureOlExtension(filename) {
21
23
  }
22
24
 
23
25
  /**
24
- * Default mock resolver (for demo use)
26
+ * Default mock resolver (for demo / fallback)
25
27
  */
26
- async function defaultMockResolver(action, context) {
27
- if (!action || typeof action !== 'string') return `[Unhandled: ${String(action)}]`;
28
+ async function defaultMockResolver(action) {
29
+ if (!action || typeof action !== 'string') return undefined;
28
30
 
29
31
  if (action.startsWith('Search for ')) {
30
32
  return {
31
- title: "HR Policy 2025",
32
- text: "Employees are entitled to 20 days of paid leave per year.",
33
- url: "mock://hr-policy"
33
+ title: 'HR Policy 2025',
34
+ text: 'Employees are entitled to 20 days of paid leave per year.',
35
+ url: 'mock://hr-policy'
34
36
  };
35
37
  }
36
38
 
37
39
  if (action.startsWith('Ask ')) {
38
- return "✅ [Mock] Summarized for demonstration.";
40
+ return '✅ [Mock] Summarized for demonstration.';
39
41
  }
40
42
 
41
43
  if (action.startsWith('Notify ')) {
@@ -48,7 +50,7 @@ async function defaultMockResolver(action, context) {
48
50
  return 'Acknowledged';
49
51
  }
50
52
 
51
- return `[Unhandled: ${action}]`;
53
+ return undefined;
52
54
  }
53
55
  defaultMockResolver.resolverName = 'defaultMockResolver';
54
56
 
@@ -67,15 +69,16 @@ async function builtInMathResolver(action, context) {
67
69
  if ((m = a.match(/^subtract\(([^,]+),\s*([^)]+)\)$/i))) return +m[1] - +m[2];
68
70
  if ((m = a.match(/^multiply\(([^,]+),\s*([^)]+)\)$/i))) return +m[1] * +m[2];
69
71
  if ((m = a.match(/^divide\(([^,]+),\s*([^)]+)\)$/i))) return +m[1] / +m[2];
70
- if ((m = a.match(/^sum\(\s*\[([^\]]+)\]\s*\)$/i)))
72
+ if ((m = a.match(/^sum\(\s*\[([^\]]+)\]\s*\)$/i))) {
71
73
  return m[1].split(',').map(Number).reduce((a, b) => a + b, 0);
74
+ }
72
75
 
73
76
  return undefined;
74
77
  }
75
78
  builtInMathResolver.resolverName = 'builtInMathResolver';
76
79
 
77
80
  /**
78
- * Resolver chaining
81
+ * Create resolver chain
79
82
  */
80
83
  function createResolverChain(resolvers, verbose = false) {
81
84
  const wrapped = async (action, context) => {
@@ -95,44 +98,59 @@ function createResolverChain(resolvers, verbose = false) {
95
98
  if (verbose) console.log(`⏭️ No resolver handled "${action}"`);
96
99
  return undefined;
97
100
  };
101
+
98
102
  wrapped._chain = resolvers;
99
103
  return wrapped;
100
104
  }
101
105
 
102
106
  /**
103
- * Load a single resolver
107
+ * Load a single resolver (CRITICAL FIX)
108
+ *
109
+ * Resolution order:
110
+ * 1. User project node_modules (npm install / npm link)
111
+ * 2. Relative/local path
112
+ * 3. Error with actionable message
104
113
  */
105
114
  function loadSingleResolver(specifier) {
106
115
  if (!specifier) throw new Error('Empty resolver specifier');
107
116
 
108
- if (specifier.endsWith('.json')) {
109
- const manifest = JSON.parse(fs.readFileSync(specifier, 'utf8'));
110
- if (manifest.protocol?.startsWith('http')) {
111
- const externalResolver = async () => undefined;
112
- externalResolver.resolverName = manifest.name;
113
- externalResolver.manifest = manifest;
114
- console.log(`🌐 Loaded external resolver: ${manifest.name}`);
115
- return externalResolver;
116
- }
117
- }
117
+ // Anchor resolution to the CALLING PROJECT, not the kernel
118
+ const projectRequire = createRequire(
119
+ path.join(process.cwd(), 'package.json')
120
+ );
118
121
 
119
122
  let resolver;
120
- const pkgName = specifier.startsWith('.') || specifier.startsWith('/')
121
- ? path.basename(specifier, path.extname(specifier))
122
- : specifier.replace(/^@[^/]+\//, '');
123
+ let resolvedFrom = 'unknown';
123
124
 
124
125
  try {
125
- resolver = require(specifier);
126
+ resolver = projectRequire(specifier);
127
+ resolvedFrom = 'project';
126
128
  } catch {
127
- resolver = require(path.resolve(process.cwd(), specifier));
129
+ try {
130
+ resolver = require(path.resolve(process.cwd(), specifier));
131
+ resolvedFrom = 'local';
132
+ } catch {
133
+ throw new Error(
134
+ `Resolver "${specifier}" not found.\n` +
135
+ `Install it in your project with:\n` +
136
+ ` npm install ${specifier}\n` +
137
+ `or link it with:\n` +
138
+ ` npm link ${specifier}`
139
+ );
140
+ }
128
141
  }
129
142
 
130
143
  if (typeof resolver !== 'function') {
131
- throw new Error(`Resolver must export a function`);
144
+ throw new Error(`Resolver "${specifier}" must export a function`);
132
145
  }
133
146
 
134
- resolver.resolverName ||= pkgName;
135
- console.log(`📦 Loaded resolver: ${resolver.resolverName}`);
147
+ const name =
148
+ resolver.resolverName ||
149
+ specifier.replace(/^@[^/]+\//, '');
150
+
151
+ resolver.resolverName = name;
152
+
153
+ console.log(`📦 Loaded resolver: ${name} (${resolvedFrom})`);
136
154
  return resolver;
137
155
  }
138
156
 
@@ -142,14 +160,21 @@ function loadSingleResolver(specifier) {
142
160
  function loadResolverChain(specifiers, verbose, allowed) {
143
161
  const resolvers = [];
144
162
 
145
- if (allowed.has('builtInMathResolver')) resolvers.push(builtInMathResolver);
163
+ if (allowed.has('builtInMathResolver')) {
164
+ resolvers.push(builtInMathResolver);
165
+ }
146
166
 
147
167
  for (const r of specifiers.map(loadSingleResolver)) {
148
- if (allowed.has(r.resolverName)) resolvers.push(r);
149
- else if (verbose) console.warn(`⚠️ Skipped disallowed resolver: ${r.resolverName}`);
168
+ if (allowed.has(r.resolverName)) {
169
+ resolvers.push(r);
170
+ } else if (verbose) {
171
+ console.warn(`⚠️ Skipped disallowed resolver: ${r.resolverName}`);
172
+ }
150
173
  }
151
174
 
152
- if (allowed.has('defaultMockResolver')) resolvers.push(defaultMockResolver);
175
+ if (allowed.has('defaultMockResolver')) {
176
+ resolvers.push(defaultMockResolver);
177
+ }
153
178
 
154
179
  return createResolverChain(resolvers, verbose);
155
180
  }
@@ -159,10 +184,8 @@ function loadResolverChain(specifiers, verbose, allowed) {
159
184
  */
160
185
  const program = new Command();
161
186
 
162
- // === ADDED: Version support (1 line added) ===
163
187
  program.version(pkg.version, '-V, --version', 'Show O-lang kernel version');
164
188
 
165
- // === RUN COMMAND ===
166
189
  program
167
190
  .name('olang')
168
191
  .command('run <file>')
@@ -175,17 +198,30 @@ program
175
198
  .option('-v, --verbose')
176
199
  .action(async (file, options) => {
177
200
  ensureOlExtension(file);
201
+
178
202
  const workflowSource = fs.readFileSync(file, 'utf8');
179
203
  const workflow = parse(workflowSource, file);
180
204
 
181
205
  const allowed = new Set(workflow.allowedResolvers);
182
- const resolver = loadResolverChain(options.resolver, options.verbose, allowed);
206
+ const resolver = loadResolverChain(
207
+ options.resolver,
208
+ options.verbose,
209
+ allowed
210
+ );
211
+
212
+ const result = await execute(
213
+ workflow,
214
+ options.input,
215
+ resolver,
216
+ options.verbose
217
+ );
183
218
 
184
- const result = await execute(workflow, options.input, resolver, options.verbose);
185
219
  console.log(JSON.stringify(result, null, 2));
186
220
  });
187
221
 
188
- // === SERVER COMMAND (✅ PROPER INTEGRATION) ===
222
+ /**
223
+ * SERVER MODE
224
+ */
189
225
  program
190
226
  .command('server')
191
227
  .description('Start O-lang kernel in HTTP server mode')
@@ -204,10 +240,6 @@ program
204
240
  try {
205
241
  const { workflowSource, inputs = {}, resolvers = [], verbose = false } = req.body;
206
242
 
207
- if (typeof workflowSource !== 'string') {
208
- return reply.status(400).send({ error: 'workflowSource must be a string' });
209
- }
210
-
211
243
  const workflow = parse(workflowSource, 'remote.ol');
212
244
  const allowed = new Set(workflow.allowedResolvers);
213
245
  const resolver = loadResolverChain(resolvers, verbose, allowed);
@@ -219,17 +251,12 @@ program
219
251
  }
220
252
  });
221
253
 
222
- const PORT = parseInt(options.port, 10);
223
- const HOST = options.host;
254
+ await fastify.listen({
255
+ port: Number(options.port),
256
+ host: options.host
257
+ });
224
258
 
225
- try {
226
- await fastify.listen({ port: PORT, host: HOST });
227
- console.log(`✅ O-Lang Kernel running on http://${HOST}:${PORT}`);
228
- } catch (err) {
229
- console.error('❌ Failed to start server:', err);
230
- process.exit(1);
231
- }
259
+ console.log(`✅ O-Lang Kernel running on http://${options.host}:${options.port}`);
232
260
  });
233
261
 
234
- // === PARSE CLI ===
235
262
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.1.8",
3
+ "version": "1.2.1",
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",
7
- "bin": {
8
- "olang": "./cli.js"
9
- },
7
+ "bin": {
8
+ "olang": "./cli/olang.js"
9
+ },
10
10
  "files": [
11
11
  "cli.js",
12
12
  "src/"
@@ -15,8 +15,10 @@
15
15
  "start": "node cli.js"
16
16
  },
17
17
  "dependencies": {
18
- "commander": "^12.0.0",
19
- "dotenv": "^17.2.3"
18
+ "commander": "^12.0.0",
19
+ "dotenv": "^17.2.3",
20
+ "lodash": "^4.17.21",
21
+ "fastify": "^4.26.0"
20
22
  },
21
23
  "keywords": [
22
24
  "agent",
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
 
3
- // ✅ Symbol normalization helper (backward compatible)
3
+ // ✅ Symbol normalization helper (backward compatible - SAFE to keep)
4
4
  function normalizeSymbol(raw) {
5
5
  if (!raw) return raw;
6
6
  // Take only the first word (stop at first whitespace)
@@ -8,17 +8,7 @@ function normalizeSymbol(raw) {
8
8
  return raw.split(/\s+/)[0].replace(/[^\w$]/g, '');
9
9
  }
10
10
 
11
- // ACTION NORMALIZATION HELPER (NEW)
12
- // Strip decorative prefixes for consistent resolver matching
13
- function normalizeAction(actionRaw) {
14
- if (typeof actionRaw !== 'string') return actionRaw;
15
-
16
- // Strip optional decorative prefixes (case-insensitive)
17
- // Preserves semantic prefixes: "Ask", "Use" (handled as separate step types)
18
- return actionRaw
19
- .replace(/^(Action|Do|Perform|Execute|Run|Call|Invoke)\s+/i, '')
20
- .trim();
21
- }
11
+ // REMOVED: normalizeAction() function (was stripping "Action" prefix → broke resolver matching)
22
12
 
23
13
  function parse(content, filename = '<unknown>') {
24
14
  if (typeof content === 'string') {
@@ -269,27 +259,24 @@ function parseWorkflowLines(lines, filename) {
269
259
  }
270
260
  }
271
261
 
272
- // Step declaration - ✅ NORMALIZE ACTION HERE
262
+ // Step declaration - ✅ PRESERVE ACTION EXACTLY (NO NORMALIZATION)
273
263
  const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
274
264
  if (stepMatch) {
275
265
  flushCurrentStep(); // ✅ Flush previous step
276
266
  const stepNumber = parseInt(stepMatch[1], 10);
277
- let stepContent = stepMatch[2].trim();
278
-
279
- // ✅ CRITICAL: Normalize action syntax BEFORE storing in AST
280
- stepContent = normalizeAction(stepContent);
267
+ const stepContent = stepMatch[2].trim(); // ← PRESERVED EXACTLY (no normalizeAction)
281
268
 
282
269
  currentStep = {
283
270
  type: 'action',
284
271
  stepNumber: stepNumber,
285
- actionRaw: stepContent, // ← CLEAN, normalized action string
272
+ actionRaw: stepContent, // ← CRITICAL: No normalization here
286
273
  saveAs: null,
287
274
  constraints: {}
288
275
  };
289
276
  continue;
290
277
  }
291
278
 
292
- // Save as - ✅ Apply normalization
279
+ // Save as - ✅ Apply normalization (safe for symbol names)
293
280
  const saveMatch = line.match(/^Save as\s+(.+)$/i);
294
281
  if (saveMatch && currentStep) {
295
282
  currentStep.saveAs = normalizeSymbol(saveMatch[1].trim());
@@ -383,13 +370,13 @@ function parseWorkflowLines(lines, filename) {
383
370
  continue;
384
371
  }
385
372
 
386
- // Use (for Notify-like actions) - ✅ NORMALIZE TOOL HERE
373
+ // Use (for Notify-like actions) - ✅ PRESERVE TOOL EXACTLY (NO NORMALIZATION)
387
374
  const useMatch = line.match(/^Use\s+(.+)$/i);
388
375
  if (useMatch) {
389
376
  flushCurrentStep();
390
377
  workflow.steps.push({
391
378
  type: 'use',
392
- tool: normalizeAction(useMatch[1].trim()), // Normalize tool name
379
+ tool: useMatch[1].trim(), // PRESERVED EXACTLY (no normalizeAction)
393
380
  stepNumber: workflow.steps.length + 1,
394
381
  saveAs: null,
395
382
  constraints: {}
@@ -397,13 +384,13 @@ function parseWorkflowLines(lines, filename) {
397
384
  continue;
398
385
  }
399
386
 
400
- // Ask (for Notify/resolver calls) - ✅ NORMALIZE TARGET HERE
387
+ // Ask (for Notify/resolver calls) - ✅ PRESERVE TARGET EXACTLY (NO NORMALIZATION)
401
388
  const askMatch = line.match(/^Ask\s+(.+)$/i);
402
389
  if (askMatch) {
403
390
  flushCurrentStep();
404
391
  workflow.steps.push({
405
392
  type: 'ask',
406
- target: normalizeAction(askMatch[1].trim()), // Normalize target
393
+ target: askMatch[1].trim(), // PRESERVED EXACTLY (no normalizeAction)
407
394
  stepNumber: workflow.steps.length + 1,
408
395
  saveAs: null,
409
396
  constraints: {}
@@ -425,12 +412,12 @@ function parseWorkflowLines(lines, filename) {
425
412
  currentStep = {
426
413
  type: 'action',
427
414
  stepNumber: workflow.steps.length + 1,
428
- actionRaw: normalizeAction(line), // Normalize fallback actions too
415
+ actionRaw: line, // PRESERVED EXACTLY (no normalizeAction)
429
416
  saveAs: null,
430
417
  constraints: {}
431
418
  };
432
419
  } else {
433
- currentStep.actionRaw += ' ' + normalizeAction(line);
420
+ currentStep.actionRaw += ' ' + line; // ← PRESERVED EXACTLY (no normalizeAction)
434
421
  }
435
422
  }
436
423
  }
@@ -466,7 +453,7 @@ function parseWorkflowLines(lines, filename) {
466
453
  return workflow;
467
454
  }
468
455
 
469
- // Parses blocks (for parallel, if, escalation levels) - ✅ UPDATED FOR NORMALIZATION
456
+ // Parses blocks (for parallel, if, escalation levels) - ✅ PRESERVE ALL FUNCTIONALITY
470
457
  function parseBlock(lines) {
471
458
  const steps = [];
472
459
  let current = null;
@@ -482,20 +469,17 @@ function parseBlock(lines) {
482
469
  line = line.trim();
483
470
  if (!line || line.startsWith('#')) continue;
484
471
 
485
- // Step declaration in block - ✅ NORMALIZE ACTION HERE
472
+ // Step declaration in block - ✅ PRESERVE ACTION EXACTLY (NO NORMALIZATION)
486
473
  const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
487
474
  if (stepMatch) {
488
475
  flush();
489
476
  const stepNumber = parseInt(stepMatch[1], 10);
490
- let stepContent = stepMatch[2].trim();
491
-
492
- // ✅ CRITICAL: Normalize action syntax in blocks too
493
- stepContent = normalizeAction(stepContent);
477
+ const stepContent = stepMatch[2].trim(); // ← PRESERVED EXACTLY
494
478
 
495
479
  current = {
496
480
  type: 'action',
497
481
  stepNumber: stepNumber,
498
- actionRaw: stepContent,
482
+ actionRaw: stepContent, // ← CRITICAL: No normalization
499
483
  saveAs: null,
500
484
  constraints: {}
501
485
  };
@@ -545,26 +529,26 @@ function parseBlock(lines) {
545
529
  continue;
546
530
  }
547
531
 
548
- // Use in block - ✅ NORMALIZE TOOL HERE
532
+ // Use in block - ✅ PRESERVE TOOL EXACTLY (NO NORMALIZATION)
549
533
  const useMatch = line.match(/^Use\s+(.+)$/i);
550
534
  if (useMatch) {
551
535
  flush();
552
536
  steps.push({
553
537
  type: 'use',
554
- tool: normalizeAction(useMatch[1].trim()), // Normalize
538
+ tool: useMatch[1].trim(), // PRESERVED EXACTLY
555
539
  saveAs: null,
556
540
  constraints: {}
557
541
  });
558
542
  continue;
559
543
  }
560
544
 
561
- // Ask in block - ✅ NORMALIZE TARGET HERE
545
+ // Ask in block - ✅ PRESERVE TARGET EXACTLY (NO NORMALIZATION)
562
546
  const askMatch = line.match(/^Ask\s+(.+)$/i);
563
547
  if (askMatch) {
564
548
  flush();
565
549
  steps.push({
566
550
  type: 'ask',
567
- target: normalizeAction(askMatch[1].trim()), // Normalize
551
+ target: askMatch[1].trim(), // PRESERVED EXACTLY
568
552
  saveAs: null,
569
553
  constraints: {}
570
554
  });
@@ -593,7 +577,7 @@ function parseBlock(lines) {
593
577
 
594
578
  // Fallback
595
579
  if (current) {
596
- current.actionRaw += ' ' + normalizeAction(line);
580
+ current.actionRaw += ' ' + line; // ← PRESERVED EXACTLY (no normalizeAction)
597
581
  }
598
582
  }
599
583
 
@@ -625,4 +609,4 @@ function validate(workflow) {
625
609
  return errors;
626
610
  }
627
611
 
628
- module.exports = { parse, parseFromFile, parseLines, validate, normalizeAction };
612
+ module.exports = { parse, parseFromFile, parseLines, validate };
@@ -1,8 +1,10 @@
1
+ // src/runtime/RuntimeAPI.js
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
 
4
5
  class RuntimeAPI {
5
6
  constructor({ verbose = false } = {}) {
7
+ console.log('✅ KERNEL FIX VERIFIED - Unwrapping active');
6
8
  this.context = {};
7
9
  this.resources = {};
8
10
  this.agentMap = {};
@@ -331,7 +333,19 @@ class RuntimeAPI {
331
333
  }
332
334
 
333
335
  // -----------------------------
334
- // Step execution
336
+ // CRITICAL FIX: Resolver output unwrapping helper
337
+ // -----------------------------
338
+ _unwrapResolverResult(result) {
339
+ // Standard O-Lang resolver contract: { output: {...} } or { error: "..." }
340
+ if (result && typeof result === 'object' && 'output' in result && result.output !== undefined) {
341
+ return result.output;
342
+ }
343
+ // Legacy resolvers might return raw values
344
+ return result;
345
+ }
346
+
347
+ // -----------------------------
348
+ // Step execution (WHERE RESOLVERS ARE INVOKED)
335
349
  // -----------------------------
336
350
  async executeStep(step, agentResolver) {
337
351
  const stepType = step.type;
@@ -420,8 +434,11 @@ class RuntimeAPI {
420
434
 
421
435
  // ✅ ACCEPT valid result immediately (non-null/non-undefined)
422
436
  if (result !== undefined && result !== null) {
437
+ // ✅ CRITICAL FIX: Save raw result for debugging (like __resolver_0)
423
438
  this.context[`__resolver_${idx}`] = result;
424
- return result;
439
+
440
+ // ✅ UNWRAP before returning to workflow logic
441
+ return this._unwrapResolverResult(result);
425
442
  }
426
443
 
427
444
  // ⚪ Resolver skipped this action (normal behavior)
@@ -521,7 +538,7 @@ class RuntimeAPI {
521
538
  }
522
539
  });
523
540
  if (!hasDocs) {
524
- errorMessage += ` → Visit https://www.npmjs.com/search?q=%40o-lang for resolver packages\n`; // ✅ FIXED
541
+ errorMessage += ` → Visit https://www.npmjs.com/search?q=%40o-lang for resolver packages\n`; // ✅ FIXED
525
542
  }
526
543
 
527
544
  errorMessage += `\n🛑 Workflow halted to prevent unsafe data propagation to LLMs.`;
@@ -554,24 +571,38 @@ class RuntimeAPI {
554
571
  }
555
572
  }
556
573
 
557
- const res = await runResolvers(action);
558
- if (step.saveAs) this.context[step.saveAs] = res;
574
+ // CRITICAL FIX: UNWRAP resolver result BEFORE saving to context
575
+ const rawResult = await runResolvers(action);
576
+ const unwrapped = this._unwrapResolverResult(rawResult);
577
+
578
+ if (step.saveAs) {
579
+ this.context[step.saveAs] = unwrapped;
580
+ }
559
581
  break;
560
582
  }
561
583
 
562
584
  case 'use': {
563
585
  // ✅ SAFE INTERPOLATION for tool name
564
586
  const tool = this._safeInterpolate(step.tool, this.context, 'tool name');
565
- const res = await runResolvers(`Use ${tool}`);
566
- if (step.saveAs) this.context[step.saveAs] = res;
587
+ const rawResult = await runResolvers(`Use ${tool}`);
588
+ const unwrapped = this._unwrapResolverResult(rawResult);
589
+
590
+ if (step.saveAs) this.context[step.saveAs] = unwrapped;
567
591
  break;
568
592
  }
569
593
 
570
594
  case 'ask': {
571
- // ✅ SAFE INTERPOLATION: CRITICAL for LLM prompts (hallucination prevention)
572
595
  const target = this._safeInterpolate(step.target, this.context, 'LLM prompt');
573
- const res = await runResolvers(`Ask ${target}`);
574
- if (step.saveAs) this.context[step.saveAs] = res;
596
+
597
+ // ADD THIS CHECK
598
+ if (/{[^}]+}/.test(target)) {
599
+ throw new Error(`[O-Lang] Unresolved variables in prompt: "${target}"`);
600
+ }
601
+
602
+ const rawResult = await runResolvers(`Ask ${target}`);
603
+ const unwrapped = this._unwrapResolverResult(rawResult);
604
+
605
+ if (step.saveAs) this.context[step.saveAs] = unwrapped;
575
606
  break;
576
607
  }
577
608
 
@@ -842,7 +873,7 @@ class RuntimeAPI {
842
873
  const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
843
874
  await db.collection(step.collection).insertOne({
844
875
  workflow_name: this.context.workflow_name || 'unknown',
845
- data: sourceValue,
876
+ data: sourceValue, // ✅ FIXED: Added property name "data" (was broken syntax)
846
877
  created_at: new Date()
847
878
  });
848
879
  break;
@@ -915,7 +946,7 @@ class RuntimeAPI {
915
946
  });
916
947
  }
917
948
 
918
- // ✅ SEMANTIC VALIDATION: For return values..
949
+ // ✅ SEMANTIC VALIDATION: For return values
919
950
  const result = {};
920
951
  for (const key of workflow.returnValues) {
921
952
  if (this._requireSemantic(key, 'return')) {
@@ -0,0 +1,4 @@
1
+ // src/runtime/index.js
2
+ const { RuntimeAPI, execute } = require('./RuntimeAPI');
3
+
4
+ module.exports = { execute, RuntimeAPI };
@@ -0,0 +1,61 @@
1
+ function getNested(obj, path) {
2
+ if (!path) return undefined;
3
+ return path.split('.').reduce((o, k) =>
4
+ o && o[k] !== undefined ? o[k] : undefined, obj);
5
+ }
6
+
7
+ const mathFunctions = {
8
+ add: (a, b) => a + b,
9
+ subtract: (a, b) => a - b,
10
+ multiply: (a, b) => a * b,
11
+ divide: (a, b) => a / b,
12
+ equals: (a, b) => a === b,
13
+ greater: (a, b) => a > b,
14
+ less: (a, b) => a < b,
15
+ sum: arr => arr.reduce((a, v) => a + v, 0),
16
+ avg: arr => arr.reduce((a, v) => a + v, 0) / arr.length,
17
+ min: arr => Math.min(...arr),
18
+ max: arr => Math.max(...arr),
19
+ increment: a => a + 1,
20
+ decrement: a => a - 1,
21
+ round: Math.round,
22
+ floor: Math.floor,
23
+ ceil: Math.ceil,
24
+ abs: Math.abs
25
+ };
26
+
27
+ function evaluateMath(expr, context, addWarning) {
28
+ expr = expr.replace(/\{([^\}]+)\}/g, (_, p) => {
29
+ const v = getNested(context, p.trim());
30
+ return v !== undefined ? v : 0;
31
+ });
32
+
33
+ try {
34
+ const fn = new Function(
35
+ ...Object.keys(mathFunctions),
36
+ `return ${expr};`
37
+ );
38
+ return fn(...Object.values(mathFunctions));
39
+ } catch (e) {
40
+ addWarning?.(`Failed to evaluate math "${expr}": ${e.message}`);
41
+ return 0;
42
+ }
43
+ }
44
+
45
+ function evaluateCondition(cond, ctx) {
46
+ cond = cond.trim();
47
+ const eq = cond.match(/^\{(.+)\}\s+equals\s+"(.*)"$/);
48
+ if (eq) return getNested(ctx, eq[1]) == eq[2];
49
+ const gt = cond.match(/^\{(.+)\}\s+greater than\s+(\d+\.?\d*)$/);
50
+ if (gt) return +getNested(ctx, gt[1]) > +gt[2];
51
+ const lt = cond.match(/^\{(.+)\}\s+less than\s+(\d+\.?\d*)$/);
52
+ if (lt) return +getNested(ctx, lt[1]) < +lt[2];
53
+ return Boolean(getNested(ctx, cond.replace(/[{}]/g, '')));
54
+ }
55
+
56
+ module.exports = {
57
+ getNested,
58
+ mathFunctions,
59
+ evaluateMath,
60
+ evaluateCondition
61
+ };
@@ -0,0 +1,267 @@
1
+ // src/runtime/resolverRunner.js
2
+ const path = require('path');
3
+
4
+ class ResolverRunner {
5
+ constructor({ verbose = false, resolvers = [] }) {
6
+ this.verbose = verbose;
7
+ this.resolvers = resolvers;
8
+ }
9
+
10
+ /**
11
+ * Parse action string into structured parameters.
12
+ * Handles BOTH quoted and unquoted values:
13
+ * "bank-account-lookup customer_id=12345 db_path=bank.db"
14
+ * "bank-account-lookup customer_id=\"12345\" db_path=\"bank.db\""
15
+ *
16
+ * @param {string} actionStr - Raw action string from workflow step
17
+ * @returns {Object|null} Parsed action with resolverName, params[], namedParams{}, and raw string
18
+ */
19
+ _parseAction(actionStr) {
20
+ if (typeof actionStr !== 'string' || !actionStr.trim()) return null;
21
+
22
+ // Normalize: Remove leading "Action"/"Ask" keywords for parsing
23
+ let normalized = actionStr.trim();
24
+ const actionMatch = normalized.match(/^(Action|Ask)\s+(.+)/i);
25
+ if (actionMatch) {
26
+ normalized = actionMatch[2].trim();
27
+ }
28
+
29
+ // Extract resolver name (first word)
30
+ const firstSpace = normalized.indexOf(' ');
31
+ const resolverName = firstSpace > 0
32
+ ? normalized.substring(0, firstSpace).toLowerCase()
33
+ : normalized.toLowerCase();
34
+
35
+ // Extract key=value pairs (handles quoted/unquoted values)
36
+ const namedParams = {};
37
+ const paramRegex = /(\w+)=(?:"([^"]*)"|(\S+))/g;
38
+ let match;
39
+ while ((match = paramRegex.exec(normalized)) !== null) {
40
+ const [, key, quotedVal, unquotedVal] = match;
41
+ namedParams[key] = quotedVal || unquotedVal;
42
+ }
43
+
44
+ // For "Ask llm-groq \"prompt\"" style - extract the quoted prompt as first param
45
+ const askMatch = normalized.match(/^(\S+)\s+"([^"]+)"$/);
46
+ if (askMatch && !Object.keys(namedParams).length) {
47
+ namedParams.prompt = askMatch[2];
48
+ }
49
+
50
+ // Build positional params array (order matters for legacy resolvers)
51
+ const params = Object.values(namedParams);
52
+
53
+ return {
54
+ resolverName,
55
+ params,
56
+ namedParams,
57
+ raw: actionStr,
58
+ normalized
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Used ONLY for logging / routing insight
64
+ */
65
+ normalizeAction(action) {
66
+ const trimmed = action.trim();
67
+ if (/^Action\s+/i.test(trimmed)) {
68
+ return trimmed.replace(/^Action\s+/i, '');
69
+ }
70
+ if (/^Ask\s+/i.test(trimmed)) {
71
+ return trimmed.replace(/^Ask\s+/i, '');
72
+ }
73
+ return trimmed;
74
+ }
75
+
76
+ /**
77
+ * Resolve placeholders {var} in text using context values
78
+ */
79
+ resolvePlaceholders(text, context) {
80
+ return text.replace(/{([^}]+)}/g, (_, expr) => {
81
+ const value = expr
82
+ .trim()
83
+ .split('.')
84
+ .reduce((acc, key) => acc?.[key], context);
85
+
86
+ if (value === undefined) {
87
+ throw new Error(
88
+ `[O-Lang SAFETY] Unresolved placeholder at runtime: {${expr}}`
89
+ );
90
+ }
91
+
92
+ // 🔒 SAFETY: Block object/array interpolation into strings
93
+ if (value !== null && typeof value === 'object') {
94
+ const type = Array.isArray(value) ? 'array' : 'object';
95
+ throw new Error(
96
+ `[O-Lang SAFETY] Cannot interpolate ${type} "{${expr}}" into action string.\n` +
97
+ ` → Use dot notation: "{${expr}.field}" (e.g., {account_info.balance})`
98
+ );
99
+ }
100
+
101
+ return String(value);
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Execute workflow plan with context mediation
107
+ */
108
+ async execute({ plan, context }) {
109
+ if (this.verbose) {
110
+ console.log('[ResolverRunner] execute called with steps:', plan.steps.length);
111
+ }
112
+
113
+ for (let i = 0; i < plan.steps.length; i++) {
114
+ const step = plan.steps[i];
115
+
116
+ if (!step.actionRaw) {
117
+ throw new Error('[O-Lang SAFETY] Step missing actionRaw');
118
+ }
119
+
120
+ // ✅ Resolve placeholders JUST IN TIME (after semantic validation)
121
+ const resolvedAction = this.resolvePlaceholders(step.actionRaw, context);
122
+ const normalized = this.normalizeAction(resolvedAction);
123
+
124
+ if (this.verbose) {
125
+ console.log(`\n[Step ${i + 1}] Raw action: "${step.actionRaw}"`);
126
+ console.log(`[Step ${i + 1}] Resolved: "${resolvedAction}"`);
127
+ }
128
+
129
+ // ✅ PARSE ACTION BEFORE RESOLVER INVOCATION (critical fix)
130
+ const parsed = this._parseAction(resolvedAction);
131
+
132
+ if (this.verbose && parsed) {
133
+ console.log(`[Step ${i + 1}] Parsed resolver: "${parsed.resolverName}"`);
134
+ console.log(`[Step ${i + 1}] Parsed params:`, parsed.namedParams);
135
+ }
136
+
137
+ let handled = false;
138
+ let resolverAttempts = [];
139
+
140
+ for (const resolver of this.resolvers) {
141
+ const resolverName = resolver?.resolverName?.toLowerCase() || 'unknown';
142
+ let result;
143
+ let invocationMethod = 'unknown';
144
+
145
+ try {
146
+ // ✅ STRATEGY 1: If resolver name matches parsed action → try POSITIONAL PARAMS (backward compatible)
147
+ if (parsed && resolverName === parsed.resolverName) {
148
+ try {
149
+ // Call with native params: resolver(customer_id, db_path, context)
150
+ result = await resolver(...parsed.params, context);
151
+ invocationMethod = 'positional_params';
152
+
153
+ if (this.verbose) {
154
+ console.log(`[ResolverRunner] ✓ Invoked "${resolverName}" via positional params`);
155
+ }
156
+ } catch (e) {
157
+ // If positional params fail, fall through to raw string invocation below
158
+ if (this.verbose) {
159
+ console.log(`[ResolverRunner] ✗ Positional params failed for "${resolverName}", trying raw string...`);
160
+ }
161
+ }
162
+ }
163
+
164
+ // ✅ STRATEGY 2: If not handled yet → try RAW STRING INVOCATION (for generic/new-style resolvers)
165
+ if (result === undefined) {
166
+ result = await resolver(resolvedAction, context);
167
+ invocationMethod = 'raw_string';
168
+
169
+ if (this.verbose && result !== undefined) {
170
+ console.log(`[ResolverRunner] ✓ Invoked "${resolverName}" via raw string`);
171
+ }
172
+ }
173
+
174
+ // ✅ ACCEPT valid result (non-undefined)
175
+ if (result !== undefined) {
176
+ handled = true;
177
+
178
+ // Handle resolver error contract
179
+ if (result?.error) {
180
+ // Parse structured error if JSON string
181
+ let errorMsg = result.error;
182
+ try {
183
+ const errObj = JSON.parse(result.error);
184
+ errorMsg = `[${errObj.code}] ${errObj.message || errObj.error}`;
185
+ } catch (e) {
186
+ // Not JSON - use as-is
187
+ }
188
+
189
+ throw new Error(`[Resolver Error] ${resolverName}: ${errorMsg}`);
190
+ }
191
+
192
+ // ✅ CRITICAL FIX: UNWRAP output BEFORE saving to context
193
+ const valueToSave = result?.output !== undefined ? result.output : result;
194
+
195
+ // Save to context if requested
196
+ if (valueToSave !== undefined && step.saveAs) {
197
+ context[step.saveAs] = valueToSave;
198
+
199
+ if (this.verbose) {
200
+ console.log(`[Step ${i + 1}] Output saved to context.${step.saveAs}:`, valueToSave);
201
+ }
202
+ }
203
+
204
+ resolverAttempts.push({
205
+ name: resolverName,
206
+ status: 'success',
207
+ method: invocationMethod
208
+ });
209
+ break;
210
+ } else {
211
+ resolverAttempts.push({
212
+ name: resolverName,
213
+ status: 'skipped',
214
+ reason: 'returned undefined'
215
+ });
216
+ }
217
+
218
+ } catch (e) {
219
+ resolverAttempts.push({
220
+ name: resolverName,
221
+ status: 'failed',
222
+ error: e.message || String(e)
223
+ });
224
+
225
+ if (this.verbose) {
226
+ console.warn(`[ResolverRunner] Resolver "${resolverName}" failed:`, e.message || e);
227
+ }
228
+ // Continue to next resolver in chain
229
+ }
230
+ }
231
+
232
+ // ✅ SAFETY: No resolver handled this action → halt workflow
233
+ if (!handled) {
234
+ let errorMessage = `[O-Lang SAFETY] No resolver handled action: "${resolvedAction}"\n\n`;
235
+ errorMessage += `Attempted resolvers:\n`;
236
+
237
+ resolverAttempts.forEach((attempt, idx) => {
238
+ const namePad = attempt.name.padEnd(30);
239
+ if (attempt.status === 'skipped') {
240
+ errorMessage += ` ${idx + 1}. ${namePad} → SKIPPED (returned undefined)\n`;
241
+ } else if (attempt.status === 'failed') {
242
+ errorMessage += ` ${idx + 1}. ${namePad} → FAILED\n`;
243
+ errorMessage += ` Error: ${attempt.error.substring(0, 80)}\n`;
244
+ } else {
245
+ errorMessage += ` ${idx + 1}. ${namePad} → ${attempt.status.toUpperCase()} (${attempt.method})\n`;
246
+ }
247
+ });
248
+
249
+ errorMessage += `\n💡 How to fix:\n`;
250
+ errorMessage += ` • Verify resolver is loaded with correct name ("${parsed?.resolverName || 'unknown'}")\n`;
251
+ errorMessage += ` • Ensure resolver package is installed and registered with kernel\n`;
252
+ errorMessage += ` • Check resolver signature matches kernel expectations:\n`;
253
+ errorMessage += ` → Legacy: resolver(param1, param2, context)\n`;
254
+ errorMessage += ` → Modern: resolver(actionString, context)\n`;
255
+ errorMessage += `\n🛑 Workflow halted to prevent unsafe data propagation.`;
256
+
257
+ throw new Error(errorMessage);
258
+ }
259
+ }
260
+
261
+ if (this.verbose) {
262
+ console.log('\n[ResolverRunner] All steps executed successfully');
263
+ }
264
+ }
265
+ }
266
+
267
+ module.exports = ResolverRunner;
@@ -0,0 +1,36 @@
1
+ // src/runtime/semantic.js
2
+ const { parse } = require('../parser');
3
+
4
+ class SemanticEngine {
5
+ constructor(options = {}) {
6
+ this.verbose = options.verbose || false;
7
+ }
8
+
9
+ analyze(workflowSource) {
10
+ let workflowPlan;
11
+
12
+ if (typeof workflowSource === 'string') {
13
+ if (this.verbose) console.log('[semantic] Parsing workflow string...');
14
+ workflowPlan = parse(workflowSource); // always parse string to workflow object
15
+ } else if (typeof workflowSource === 'object' && workflowSource !== null) {
16
+ workflowPlan = workflowSource;
17
+ // Ensure steps array exists
18
+ if (!Array.isArray(workflowPlan.steps)) workflowPlan.steps = [];
19
+ } else {
20
+ throw new Error('[semantic] Invalid workflow input');
21
+ }
22
+
23
+ // Ensure safe defaults
24
+ if (!workflowPlan.steps) workflowPlan.steps = [];
25
+ if (!workflowPlan.allowedResolvers) workflowPlan.allowedResolvers = [];
26
+ if (!workflowPlan.returnValues) workflowPlan.returnValues = [];
27
+
28
+ if (this.verbose) {
29
+ console.log(`[semantic] Workflow "${workflowPlan.name || '<unknown>'}" analyzed: ${workflowPlan.steps.length} steps`);
30
+ }
31
+
32
+ return workflowPlan;
33
+ }
34
+ }
35
+
36
+ module.exports = SemanticEngine;
@@ -0,0 +1,62 @@
1
+ // src/runtime/transport/http.js
2
+
3
+ /**
4
+ * Existing function — KEPT AS-IS
5
+ * Backward compatible
6
+ */
7
+ async function callExternalResolver(resolver, action, context) {
8
+ const { endpoint, timeout_ms = 30000 } = resolver.manifest;
9
+
10
+ const controller = new AbortController();
11
+ const timer = setTimeout(() => controller.abort(), timeout_ms);
12
+
13
+ try {
14
+ const res = await fetch(`${endpoint}/resolve`, {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/json' },
17
+ body: JSON.stringify({
18
+ action,
19
+ context,
20
+ resolver: resolver.resolverName,
21
+ workflow: context.workflow_name,
22
+ timestamp: new Date().toISOString()
23
+ }),
24
+ signal: controller.signal
25
+ });
26
+
27
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
28
+
29
+ const json = await res.json();
30
+ if (json?.error) throw new Error(json.error.message);
31
+
32
+ return json.result;
33
+ } finally {
34
+ clearTimeout(timer);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * ✅ NEW: Class wrapper expected by RuntimeAPI
40
+ */
41
+ class HttpTransport {
42
+ constructor({ verbose = false } = {}) {
43
+ this.verbose = verbose;
44
+ }
45
+
46
+ async call(resolver, action, context) {
47
+ if (this.verbose) {
48
+ console.log(
49
+ `[transport:http] calling external resolver "${resolver.resolverName}"`
50
+ );
51
+ }
52
+ return callExternalResolver(resolver, action, context);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * ✅ EXPORTS
58
+ * - default export: class (for RuntimeAPI)
59
+ * - named export: function (for existing code)
60
+ */
61
+ module.exports = HttpTransport;
62
+ module.exports.callExternalResolver = callExternalResolver;
File without changes