@o-lang/olang 1.0.2 → 1.0.4
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 +88 -22
- package/package.json +1 -1
- package/src/parser.js +12 -13
- package/src/runtime.js +61 -3
package/cli.js
CHANGED
|
@@ -5,6 +5,21 @@ const { execute } = require('./src/runtime');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Enforce .ol extension ONLY
|
|
10
|
+
*/
|
|
11
|
+
function ensureOlExtension(filename) {
|
|
12
|
+
if (!filename.endsWith('.ol')) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Invalid file: "${filename}".\n` +
|
|
15
|
+
`O-Lang workflows must use the ".ol" extension.`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default mock resolver (for demo use)
|
|
22
|
+
*/
|
|
8
23
|
async function defaultMockResolver(action, context) {
|
|
9
24
|
if (action.startsWith('Search for ')) {
|
|
10
25
|
return {
|
|
@@ -13,20 +28,55 @@ async function defaultMockResolver(action, context) {
|
|
|
13
28
|
url: "mock://hr-policy"
|
|
14
29
|
};
|
|
15
30
|
}
|
|
31
|
+
|
|
16
32
|
if (action.startsWith('Ask ')) {
|
|
17
|
-
return "✅ [Mock] Summarized for
|
|
33
|
+
return "✅ [Mock] Summarized for demonstration.";
|
|
18
34
|
}
|
|
35
|
+
|
|
19
36
|
if (action.startsWith('Notify ')) {
|
|
20
37
|
const recipient = action.match(/Notify (\S+)/)?.[1] || 'user@example.com';
|
|
21
38
|
return `📬 Notification sent to ${recipient}`;
|
|
22
39
|
}
|
|
40
|
+
|
|
23
41
|
if (action.startsWith('Debrief ') || action.startsWith('Evolve ')) {
|
|
24
42
|
console.log(`[O-Lang] ${action}`);
|
|
25
43
|
return 'Acknowledged';
|
|
26
44
|
}
|
|
45
|
+
|
|
27
46
|
return `[Unhandled: ${action}]`;
|
|
28
47
|
}
|
|
29
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Built-in Math Resolver
|
|
51
|
+
* Supports minimal math operations for workflows
|
|
52
|
+
*/
|
|
53
|
+
async function builtInMathResolver(action, context) {
|
|
54
|
+
// Replace variables in context
|
|
55
|
+
action = action.replace(/\{([^\}]+)\}/g, (_, key) => {
|
|
56
|
+
const val = context[key.trim()];
|
|
57
|
+
return val !== undefined ? val : `{${key}}`;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let match;
|
|
61
|
+
|
|
62
|
+
match = action.match(/^add\(([^,]+),\s*([^)]+)\)$/);
|
|
63
|
+
if (match) return parseFloat(match[1]) + parseFloat(match[2]);
|
|
64
|
+
|
|
65
|
+
match = action.match(/^subtract\(([^,]+),\s*([^)]+)\)$/);
|
|
66
|
+
if (match) return parseFloat(match[1]) - parseFloat(match[2]);
|
|
67
|
+
|
|
68
|
+
match = action.match(/^multiply\(([^,]+),\s*([^)]+)\)$/);
|
|
69
|
+
if (match) return parseFloat(match[1]) * parseFloat(match[2]);
|
|
70
|
+
|
|
71
|
+
match = action.match(/^divide\(([^,]+),\s*([^)]+)\)$/);
|
|
72
|
+
if (match) return parseFloat(match[1]) / parseFloat(match[2]);
|
|
73
|
+
|
|
74
|
+
return null; // not handled
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolver chaining mechanism
|
|
79
|
+
*/
|
|
30
80
|
function createResolverChain(resolvers) {
|
|
31
81
|
return async (action, context) => {
|
|
32
82
|
for (const resolver of resolvers) {
|
|
@@ -61,42 +111,65 @@ function loadSingleResolver(specifier) {
|
|
|
61
111
|
console.log(`📁 Loaded resolver: ${absolutePath}`);
|
|
62
112
|
return resolver;
|
|
63
113
|
} catch (e2) {
|
|
64
|
-
throw new Error(
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Failed to load resolver '${specifier}':\n npm: ${e1.message}\n file: ${e2.message}`
|
|
116
|
+
);
|
|
65
117
|
}
|
|
66
118
|
}
|
|
67
119
|
}
|
|
68
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Updated resolver chain
|
|
123
|
+
* Built-in math resolver is added first, then user resolvers, then default mock
|
|
124
|
+
*/
|
|
69
125
|
function loadResolverChain(specifiers) {
|
|
126
|
+
const userResolvers = specifiers?.map(loadSingleResolver) || [];
|
|
127
|
+
const resolvers = [builtInMathResolver, ...userResolvers, defaultMockResolver];
|
|
128
|
+
|
|
70
129
|
if (!specifiers || specifiers.length === 0) {
|
|
71
|
-
console.log('ℹ️ No resolver provided. Using default mock resolver.');
|
|
72
|
-
|
|
130
|
+
console.log('ℹ️ No resolver provided. Using built-in math + default mock resolver.');
|
|
131
|
+
} else {
|
|
132
|
+
console.log(`📦 Loaded user resolvers: ${specifiers.join(', ')}`);
|
|
73
133
|
}
|
|
74
134
|
|
|
75
|
-
const resolvers = specifiers.map(loadSingleResolver);
|
|
76
135
|
return createResolverChain(resolvers);
|
|
77
136
|
}
|
|
78
137
|
|
|
138
|
+
/**
|
|
139
|
+
* CLI Setup
|
|
140
|
+
*/
|
|
79
141
|
const program = new Command();
|
|
80
142
|
|
|
81
143
|
program
|
|
82
144
|
.name('olang')
|
|
83
|
-
.description('O-Lang CLI: run .
|
|
145
|
+
.description('O-Lang CLI: run .ol workflows with rule-enforced agent governance')
|
|
84
146
|
.command('run <file>')
|
|
85
|
-
.option(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
147
|
+
.option(
|
|
148
|
+
'-r, --resolver <specifier>',
|
|
149
|
+
'Resolver (npm package or local path). Can be used multiple times.\nExample:\n -r @o-lang/llm-groq\n -r @o-lang/notify-telegram',
|
|
150
|
+
(val, acc) => { acc.push(val); return acc; },
|
|
151
|
+
[]
|
|
152
|
+
)
|
|
153
|
+
.option(
|
|
154
|
+
'-i, --input <k=v>',
|
|
155
|
+
'Input parameters',
|
|
156
|
+
(val, acc = {}) => {
|
|
157
|
+
const [k, v] = val.split('=');
|
|
158
|
+
acc[k] = isNaN(v) ? v : parseFloat(v);
|
|
159
|
+
return acc;
|
|
160
|
+
},
|
|
161
|
+
{}
|
|
162
|
+
)
|
|
94
163
|
.action(async (file, options) => {
|
|
95
164
|
try {
|
|
165
|
+
ensureOlExtension(file);
|
|
166
|
+
|
|
96
167
|
const content = fs.readFileSync(file, 'utf8');
|
|
97
168
|
const workflow = parse(content);
|
|
169
|
+
|
|
98
170
|
const resolver = loadResolverChain(options.resolver);
|
|
99
171
|
const result = await execute(workflow, options.input, resolver);
|
|
172
|
+
|
|
100
173
|
console.log('\n=== Workflow Result ===');
|
|
101
174
|
console.log(JSON.stringify(result, null, 2));
|
|
102
175
|
} catch (err) {
|
|
@@ -106,10 +179,3 @@ program
|
|
|
106
179
|
});
|
|
107
180
|
|
|
108
181
|
program.parse(process.argv);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// src/parser.js
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
function parse(code, fileName = null) {
|
|
4
|
+
// --- New: Enforce .ol extension if filename provided ---
|
|
5
|
+
if (fileName && !fileName.endsWith(".ol")) {
|
|
6
|
+
throw new Error(`Expected .ol workflow, got: ${fileName}`);
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
const lines = code
|
|
4
10
|
.split(/\r?\n/)
|
|
5
11
|
.map(l => l.trim())
|
|
@@ -59,16 +65,11 @@ function parse(code) {
|
|
|
59
65
|
let key = eq[1].trim();
|
|
60
66
|
let value = eq[2].trim();
|
|
61
67
|
|
|
62
|
-
// Parse list: [a, b, c]
|
|
63
68
|
if (value.startsWith('[') && value.endsWith(']')) {
|
|
64
69
|
value = value.slice(1, -1).split(',').map(v => v.trim().replace(/^"/, '').replace(/"$/, ''));
|
|
65
|
-
}
|
|
66
|
-
// Parse number
|
|
67
|
-
else if (!isNaN(value)) {
|
|
70
|
+
} else if (!isNaN(value)) {
|
|
68
71
|
value = Number(value);
|
|
69
|
-
}
|
|
70
|
-
// Keep string (remove quotes if present)
|
|
71
|
-
else if (value.startsWith('"') && value.endsWith('"')) {
|
|
72
|
+
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
72
73
|
value = value.slice(1, -1);
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -119,7 +120,7 @@ function parse(code) {
|
|
|
119
120
|
continue;
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
// Agent
|
|
123
|
+
// Agent uses
|
|
123
124
|
const agentUseMatch = line.match(/^Agent\s+"([^"]+)"\s+uses\s+"([^"]+)"$/i);
|
|
124
125
|
if (agentUseMatch) {
|
|
125
126
|
workflow.steps.push({
|
|
@@ -199,7 +200,7 @@ function parse(code) {
|
|
|
199
200
|
continue;
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
//
|
|
203
|
+
// Use <Tool>
|
|
203
204
|
const useMatch = line.match(/^Use\s+(.+)$/i);
|
|
204
205
|
if (useMatch) {
|
|
205
206
|
workflow.steps.push({
|
|
@@ -212,7 +213,7 @@ function parse(code) {
|
|
|
212
213
|
continue;
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
//
|
|
216
|
+
// Ask <Target>
|
|
216
217
|
const askMatch = line.match(/^Ask\s+(.+)$/i);
|
|
217
218
|
if (askMatch) {
|
|
218
219
|
workflow.steps.push({
|
|
@@ -268,13 +269,11 @@ function parseBlock(lines) {
|
|
|
268
269
|
steps.push({ type: 'prompt', question: promptMatch[1], saveAs: null });
|
|
269
270
|
}
|
|
270
271
|
|
|
271
|
-
// New: Use <Tool>
|
|
272
272
|
const useMatch = line.match(/^Use\s+(.+)$/i);
|
|
273
273
|
if (useMatch) {
|
|
274
274
|
steps.push({ type: 'use', tool: useMatch[1].trim(), saveAs: null, constraints: {} });
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
// New: Ask <Target>
|
|
278
277
|
const askMatch = line.match(/^Ask\s+(.+)$/i);
|
|
279
278
|
if (askMatch) {
|
|
280
279
|
steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
|
package/src/runtime.js
CHANGED
|
@@ -24,9 +24,11 @@ class RuntimeAPI {
|
|
|
24
24
|
evaluateCondition(cond, ctx) {
|
|
25
25
|
cond = cond.trim();
|
|
26
26
|
const eq = cond.match(/^\{(.+)\}\s+equals\s+"(.*)"$/);
|
|
27
|
-
if (eq) return this.getNested(ctx, eq[1])
|
|
27
|
+
if (eq) return this.getNested(ctx, eq[1]) == eq[2];
|
|
28
28
|
const gt = cond.match(/^\{(.+)\}\s+greater than\s+(\d+\.?\d*)$/);
|
|
29
29
|
if (gt) return parseFloat(this.getNested(ctx, gt[1])) > parseFloat(gt[2]);
|
|
30
|
+
const lt = cond.match(/^\{(.+)\}\s+less than\s+(\d+\.?\d*)$/);
|
|
31
|
+
if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(lt[2]);
|
|
30
32
|
return Boolean(this.getNested(ctx, cond.replace(/\{|\}/g, '')));
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -45,8 +47,66 @@ class RuntimeAPI {
|
|
|
45
47
|
return null;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
// --------------------------
|
|
51
|
+
// Math helper functions
|
|
52
|
+
// --------------------------
|
|
53
|
+
mathFunctions = {
|
|
54
|
+
add: (a, b) => a + b,
|
|
55
|
+
subtract: (a, b) => a - b,
|
|
56
|
+
multiply: (a, b) => a * b,
|
|
57
|
+
divide: (a, b) => a / b,
|
|
58
|
+
equals: (a, b) => a === b,
|
|
59
|
+
greater: (a, b) => a > b,
|
|
60
|
+
less: (a, b) => a < b,
|
|
61
|
+
sum: arr => arr.reduce((acc, val) => acc + val, 0),
|
|
62
|
+
avg: arr => arr.reduce((acc, val) => acc + val, 0) / arr.length,
|
|
63
|
+
min: arr => Math.min(...arr),
|
|
64
|
+
max: arr => Math.max(...arr),
|
|
65
|
+
increment: a => a + 1,
|
|
66
|
+
decrement: a => a - 1,
|
|
67
|
+
round: a => Math.round(a),
|
|
68
|
+
floor: a => Math.floor(a),
|
|
69
|
+
ceil: a => Math.ceil(a),
|
|
70
|
+
abs: a => Math.abs(a)
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
evaluateMath(expr) {
|
|
74
|
+
// Replace context variables in curly braces
|
|
75
|
+
expr = expr.replace(/\{([^\}]+)\}/g, (_, path) => {
|
|
76
|
+
const value = this.getNested(this.context, path.trim());
|
|
77
|
+
return value !== undefined ? value : 0;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Create a function for supported math functions only
|
|
81
|
+
const funcNames = Object.keys(this.mathFunctions);
|
|
82
|
+
const safeFunc = {};
|
|
83
|
+
funcNames.forEach(fn => {
|
|
84
|
+
safeFunc[fn] = this.mathFunctions[fn];
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// eslint-disable-next-line no-new-func
|
|
89
|
+
const f = new Function(...funcNames, `return ${expr};`);
|
|
90
|
+
return f(...funcNames.map(fn => safeFunc[fn]));
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.warn(`[O-Lang] Failed to evaluate math expression "${expr}": ${e.message}`);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --------------------------
|
|
98
|
+
// Execute workflow step
|
|
99
|
+
// --------------------------
|
|
48
100
|
async executeStep(step, agentResolver) {
|
|
49
101
|
switch (step.type) {
|
|
102
|
+
case 'calculate': {
|
|
103
|
+
// e.g., "add({x}, {y})"
|
|
104
|
+
const expr = step.expression || step.actionRaw;
|
|
105
|
+
const result = this.evaluateMath(expr);
|
|
106
|
+
if (step.saveAs) this.context[step.saveAs] = result;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
50
110
|
case 'action': {
|
|
51
111
|
const action = step.actionRaw.replace(/\{([^\}]+)\}/g, (_, path) => {
|
|
52
112
|
const value = this.getNested(this.context, path.trim());
|
|
@@ -58,14 +118,12 @@ class RuntimeAPI {
|
|
|
58
118
|
}
|
|
59
119
|
|
|
60
120
|
case 'use': {
|
|
61
|
-
// "Use <Tool>" step
|
|
62
121
|
const res = await agentResolver(`Use ${step.tool}`, this.context);
|
|
63
122
|
if (step.saveAs) this.context[step.saveAs] = res;
|
|
64
123
|
break;
|
|
65
124
|
}
|
|
66
125
|
|
|
67
126
|
case 'ask': {
|
|
68
|
-
// "Ask <Target>" step
|
|
69
127
|
const res = await agentResolver(`Ask ${step.target}`, this.context);
|
|
70
128
|
if (step.saveAs) this.context[step.saveAs] = res;
|
|
71
129
|
break;
|