@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.
- package/{cli.js → cli/olang.js} +83 -56
- package/package.json +10 -4
- package/src/{runtime.js → runtime/RuntimeAPI.js} +86 -29
- package/src/runtime/index.js +4 -0
- package/src/runtime/math.js +61 -0
- package/src/runtime/resolverRunner.js +267 -0
- package/src/runtime/semantic.js +36 -0
- package/src/runtime/transport/http.js +62 -0
- package/src/server/http.js +0 -0
- /package/src/{parser.js → parser/index.js} +0 -0
package/{cli.js → cli/olang.js}
RENAMED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
const { Command } = require('commander');
|
|
3
|
-
const { parse } = require('
|
|
4
|
-
const { execute } = require('
|
|
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
|
-
// ===
|
|
9
|
-
const pkg = require('
|
|
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
|
|
26
|
+
* Default mock resolver (for demo / fallback)
|
|
25
27
|
*/
|
|
26
|
-
async function defaultMockResolver(action
|
|
27
|
-
if (!action || typeof action !== 'string') return
|
|
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:
|
|
32
|
-
text:
|
|
33
|
-
url:
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
121
|
-
? path.basename(specifier, path.extname(specifier))
|
|
122
|
-
: specifier.replace(/^@[^/]+\//, '');
|
|
123
|
+
let resolvedFrom = 'unknown';
|
|
123
124
|
|
|
124
125
|
try {
|
|
125
|
-
resolver =
|
|
126
|
+
resolver = projectRequire(specifier);
|
|
127
|
+
resolvedFrom = 'project';
|
|
126
128
|
} catch {
|
|
127
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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'))
|
|
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))
|
|
149
|
-
|
|
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'))
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
254
|
+
await fastify.listen({
|
|
255
|
+
port: Number(options.port),
|
|
256
|
+
host: options.host
|
|
257
|
+
});
|
|
224
258
|
|
|
225
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
|
566
|
-
|
|
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
|
-
|
|
574
|
-
|
|
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,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
|