@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.
- package/{cli.js → cli/olang.js} +83 -56
- package/package.json +8 -6
- package/src/{parser.js → parser/index.js} +22 -38
- package/src/{runtime.js → runtime/RuntimeAPI.js} +43 -12
- 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/{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,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o-lang/olang",
|
|
3
|
-
"version": "1.1
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
//
|
|
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 - ✅
|
|
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
|
-
|
|
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, // ←
|
|
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) - ✅
|
|
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:
|
|
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) - ✅
|
|
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:
|
|
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:
|
|
415
|
+
actionRaw: line, // ← PRESERVED EXACTLY (no normalizeAction)
|
|
429
416
|
saveAs: null,
|
|
430
417
|
constraints: {}
|
|
431
418
|
};
|
|
432
419
|
} else {
|
|
433
|
-
currentStep.actionRaw += ' ' + normalizeAction
|
|
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) - ✅
|
|
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 - ✅
|
|
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
|
-
|
|
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 - ✅
|
|
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:
|
|
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 - ✅
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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.`;
|
|
@@ -554,24 +571,38 @@ class RuntimeAPI {
|
|
|
554
571
|
}
|
|
555
572
|
}
|
|
556
573
|
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
566
|
-
|
|
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
|
-
|
|
574
|
-
|
|
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,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
|