@o-lang/olang 1.1.9 → 1.2.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.
@@ -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,22 +1,25 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.1.9",
3
+ "version": "1.2.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",
7
7
  "bin": {
8
- "olang": "./cli.js"
8
+ "olang": "./cli/olang.js"
9
9
  },
10
10
  "files": [
11
11
  "cli.js",
12
12
  "src/"
13
13
  ],
14
14
  "scripts": {
15
- "start": "node cli.js"
15
+ "start": "node cli.js",
16
+ "test": "jest"
16
17
  },
17
18
  "dependencies": {
18
19
  "commander": "^12.0.0",
19
- "dotenv": "^17.2.3"
20
+ "dotenv": "^17.2.3",
21
+ "fastify": "^4.26.0",
22
+ "lodash": "^4.17.21"
20
23
  },
21
24
  "keywords": [
22
25
  "agent",
@@ -35,5 +38,8 @@
35
38
  "homepage": "https://github.com/O-Lang-Central/olang-kernel",
36
39
  "publishConfig": {
37
40
  "access": "public"
41
+ },
42
+ "devDependencies": {
43
+ "jest": "^30.2.0"
38
44
  }
39
45
  }
@@ -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.`;
@@ -535,43 +552,83 @@ class RuntimeAPI {
535
552
  break;
536
553
  }
537
554
 
538
- case 'action': {
539
- // SAFE INTERPOLATION: Block object→string coercion BEFORE resolver invocation
540
- const action = this._safeInterpolate(step.actionRaw, this.context, 'action step');
555
+ case 'action': {
556
+ // 🔒 Interpolate workflow variables first
557
+ let action = this._safeInterpolate(
558
+ step.actionRaw,
559
+ this.context,
560
+ 'action step'
561
+ );
541
562
 
542
- const mathCall = action.match(/^(add|subtract|multiply|divide|sum|avg|min|max|round|floor|ceil|abs)\((.*)\)$/i);
543
- if (mathCall) {
544
- const fn = mathCall[1].toLowerCase();
545
- const args = mathCall[2].split(',').map(s => {
546
- s = s.trim();
547
- if (!isNaN(s)) return parseFloat(s);
548
- return this.getNested(this.context, s.replace(/^\{|\}$/g, ''));
549
- });
550
- if (this.mathFunctions[fn]) {
551
- const value = this.mathFunctions[fn](...args);
552
- if (step.saveAs) this.context[step.saveAs] = value;
553
- break;
554
- }
555
- }
563
+ // CANONICALIZATION: Normalize DSL verbs → runtime Action
564
+ if (action.startsWith('Ask ')) {
565
+ action = 'Action ' + action.slice(4);
566
+ } else if (action.startsWith('Use ')) {
567
+ action = 'Action ' + action.slice(4);
568
+ }
569
+
570
+ // ❌ Reject non-canonical runtime actions early
571
+ if (!action.startsWith('Action ')) {
572
+ throw new Error(
573
+ `[O-Lang SAFETY] Non-canonical action received: "${action}"\n` +
574
+ ` → Expected format: Action <resolver> <args>\n` +
575
+ ` → This indicates a kernel or workflow authoring error.`
576
+ );
577
+ }
578
+
579
+ // ✅ Inline math support (language feature)
580
+ const mathCall = action.match(
581
+ /^(add|subtract|multiply|divide|sum|avg|min|max|round|floor|ceil|abs)\((.*)\)$/i
582
+ );
583
+
584
+ if (mathCall) {
585
+ const fn = mathCall[1].toLowerCase();
586
+ const args = mathCall[2].split(',').map(s => {
587
+ s = s.trim();
588
+ if (!isNaN(s)) return parseFloat(s);
589
+ return this.getNested(this.context, s.replace(/^\{|\}$/g, ''));
590
+ });
591
+
592
+ if (this.mathFunctions[fn]) {
593
+ const value = this.mathFunctions[fn](...args);
594
+ if (step.saveAs) this.context[step.saveAs] = value;
595
+ break;
596
+ }
597
+ }
598
+
599
+ // ✅ Resolver dispatch receives ONLY canonical actions
600
+ const rawResult = await runResolvers(action);
601
+ const unwrapped = this._unwrapResolverResult(rawResult);
602
+
603
+ if (step.saveAs) {
604
+ this.context[step.saveAs] = unwrapped;
605
+ }
606
+ break;
607
+ }
556
608
 
557
- const res = await runResolvers(action);
558
- if (step.saveAs) this.context[step.saveAs] = res;
559
- break;
560
- }
561
609
 
562
610
  case 'use': {
563
611
  // ✅ SAFE INTERPOLATION for tool name
564
612
  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;
613
+ const rawResult = await runResolvers(`Use ${tool}`);
614
+ const unwrapped = this._unwrapResolverResult(rawResult);
615
+
616
+ if (step.saveAs) this.context[step.saveAs] = unwrapped;
567
617
  break;
568
618
  }
569
619
 
570
620
  case 'ask': {
571
- // ✅ SAFE INTERPOLATION: CRITICAL for LLM prompts (hallucination prevention)
572
621
  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;
622
+
623
+ // ADD THIS CHECK
624
+ if (/{[^}]+}/.test(target)) {
625
+ throw new Error(`[O-Lang] Unresolved variables in prompt: "${target}"`);
626
+ }
627
+
628
+ const rawResult = await runResolvers(`Ask ${target}`);
629
+ const unwrapped = this._unwrapResolverResult(rawResult);
630
+
631
+ if (step.saveAs) this.context[step.saveAs] = unwrapped;
575
632
  break;
576
633
  }
577
634
 
@@ -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
File without changes