@o-lang/olang 1.0.1 → 1.0.3
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 +50 -19
- package/package.json +16 -5
- package/src/parser.js +53 -13
- package/src/runtime.js +18 -12
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,27 @@ 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
|
+
* Resolver chaining mechanism
|
|
51
|
+
*/
|
|
30
52
|
function createResolverChain(resolvers) {
|
|
31
53
|
return async (action, context) => {
|
|
32
54
|
for (const resolver of resolvers) {
|
|
@@ -61,7 +83,9 @@ function loadSingleResolver(specifier) {
|
|
|
61
83
|
console.log(`📁 Loaded resolver: ${absolutePath}`);
|
|
62
84
|
return resolver;
|
|
63
85
|
} catch (e2) {
|
|
64
|
-
throw new Error(
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Failed to load resolver '${specifier}':\n npm: ${e1.message}\n file: ${e2.message}`
|
|
88
|
+
);
|
|
65
89
|
}
|
|
66
90
|
}
|
|
67
91
|
}
|
|
@@ -76,27 +100,41 @@ function loadResolverChain(specifiers) {
|
|
|
76
100
|
return createResolverChain(resolvers);
|
|
77
101
|
}
|
|
78
102
|
|
|
103
|
+
/**
|
|
104
|
+
* CLI Setup
|
|
105
|
+
*/
|
|
79
106
|
const program = new Command();
|
|
80
107
|
|
|
81
108
|
program
|
|
82
109
|
.name('olang')
|
|
83
|
-
.description('O-Lang CLI: run .
|
|
110
|
+
.description('O-Lang CLI: run .ol workflows with rule-enforced agent governance')
|
|
84
111
|
.command('run <file>')
|
|
85
|
-
.option(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
112
|
+
.option(
|
|
113
|
+
'-r, --resolver <specifier>',
|
|
114
|
+
'Resolver (npm package or local path). Can be used multiple times.\nExample:\n -r @o-lang/llm-groq\n -r @o-lang/notify-telegram',
|
|
115
|
+
(val, acc) => { acc.push(val); return acc; },
|
|
116
|
+
[]
|
|
117
|
+
)
|
|
118
|
+
.option(
|
|
119
|
+
'-i, --input <k=v>',
|
|
120
|
+
'Input parameters',
|
|
121
|
+
(val, acc = {}) => {
|
|
122
|
+
const [k, v] = val.split('=');
|
|
123
|
+
acc[k] = v;
|
|
124
|
+
return acc;
|
|
125
|
+
},
|
|
126
|
+
{}
|
|
127
|
+
)
|
|
94
128
|
.action(async (file, options) => {
|
|
95
129
|
try {
|
|
130
|
+
ensureOlExtension(file);
|
|
131
|
+
|
|
96
132
|
const content = fs.readFileSync(file, 'utf8');
|
|
97
133
|
const workflow = parse(content);
|
|
134
|
+
|
|
98
135
|
const resolver = loadResolverChain(options.resolver);
|
|
99
136
|
const result = await execute(workflow, options.input, resolver);
|
|
137
|
+
|
|
100
138
|
console.log('\n=== Workflow Result ===');
|
|
101
139
|
console.log(JSON.stringify(result, null, 2));
|
|
102
140
|
} catch (err) {
|
|
@@ -106,10 +144,3 @@ program
|
|
|
106
144
|
});
|
|
107
145
|
|
|
108
146
|
program.parse(process.argv);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o-lang/olang",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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",
|
|
@@ -18,11 +18,22 @@
|
|
|
18
18
|
"commander": "^12.0.0",
|
|
19
19
|
"dotenv": "^17.2.3"
|
|
20
20
|
},
|
|
21
|
-
"keywords": [
|
|
21
|
+
"keywords": [
|
|
22
|
+
"agent",
|
|
23
|
+
"governance",
|
|
24
|
+
"workflow",
|
|
25
|
+
"llm",
|
|
26
|
+
"automation",
|
|
27
|
+
"olang",
|
|
28
|
+
"ai"
|
|
29
|
+
],
|
|
22
30
|
"license": "MIT",
|
|
23
31
|
"repository": {
|
|
24
32
|
"type": "git",
|
|
25
|
-
"url": "https://github.com/O-Lang-Central/olang-kernel.git"
|
|
33
|
+
"url": "git+https://github.com/O-Lang-Central/olang-kernel.git"
|
|
26
34
|
},
|
|
27
|
-
"homepage": "https://github.com/O-Lang-Central/olang-kernel"
|
|
28
|
-
|
|
35
|
+
"homepage": "https://github.com/O-Lang-Central/olang-kernel",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|
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())
|
|
@@ -33,7 +39,7 @@ function parse(code) {
|
|
|
33
39
|
stepNumber: parseInt(stepMatch[1], 10),
|
|
34
40
|
actionRaw: stepMatch[2].trim(),
|
|
35
41
|
saveAs: null,
|
|
36
|
-
constraints: {}
|
|
42
|
+
constraints: {}
|
|
37
43
|
});
|
|
38
44
|
i++;
|
|
39
45
|
continue;
|
|
@@ -47,7 +53,7 @@ function parse(code) {
|
|
|
47
53
|
continue;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
// Constraint
|
|
56
|
+
// Constraint
|
|
51
57
|
const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
|
|
52
58
|
if (constraintMatch && workflow.steps.length > 0) {
|
|
53
59
|
const lastStep = workflow.steps[workflow.steps.length - 1];
|
|
@@ -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
|
|
|
@@ -88,7 +89,7 @@ function parse(code) {
|
|
|
88
89
|
body.push(lines[i]);
|
|
89
90
|
i++;
|
|
90
91
|
}
|
|
91
|
-
if (i < lines.length) i++;
|
|
92
|
+
if (i < lines.length) i++;
|
|
92
93
|
workflow.steps.push({ type: 'if', condition, body: parseBlock(body) });
|
|
93
94
|
continue;
|
|
94
95
|
}
|
|
@@ -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,6 +200,32 @@ function parse(code) {
|
|
|
199
200
|
continue;
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
// Use <Tool>
|
|
204
|
+
const useMatch = line.match(/^Use\s+(.+)$/i);
|
|
205
|
+
if (useMatch) {
|
|
206
|
+
workflow.steps.push({
|
|
207
|
+
type: 'use',
|
|
208
|
+
tool: useMatch[1].trim(),
|
|
209
|
+
saveAs: null,
|
|
210
|
+
constraints: {}
|
|
211
|
+
});
|
|
212
|
+
i++;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Ask <Target>
|
|
217
|
+
const askMatch = line.match(/^Ask\s+(.+)$/i);
|
|
218
|
+
if (askMatch) {
|
|
219
|
+
workflow.steps.push({
|
|
220
|
+
type: 'ask',
|
|
221
|
+
target: askMatch[1].trim(),
|
|
222
|
+
saveAs: null,
|
|
223
|
+
constraints: {}
|
|
224
|
+
});
|
|
225
|
+
i++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
202
229
|
i++;
|
|
203
230
|
}
|
|
204
231
|
|
|
@@ -221,25 +248,38 @@ function parseBlock(lines) {
|
|
|
221
248
|
steps.push(current);
|
|
222
249
|
continue;
|
|
223
250
|
}
|
|
251
|
+
|
|
224
252
|
const saveMatch = line.match(/^Save as\s+(.+)$/i);
|
|
225
253
|
if (saveMatch && current) {
|
|
226
254
|
current.saveAs = saveMatch[1].trim();
|
|
227
255
|
}
|
|
256
|
+
|
|
228
257
|
const debriefMatch = line.match(/^Debrief\s+(\w+)\s+with\s+"(.+)"$/i);
|
|
229
258
|
if (debriefMatch) {
|
|
230
259
|
steps.push({ type: 'debrief', agent: debriefMatch[1], message: debriefMatch[2] });
|
|
231
260
|
}
|
|
261
|
+
|
|
232
262
|
const evolveMatch = line.match(/^Evolve\s+(\w+)\s+using\s+feedback:\s+"(.+)"$/i);
|
|
233
263
|
if (evolveMatch) {
|
|
234
264
|
steps.push({ type: 'evolve', agent: evolveMatch[1], feedback: evolveMatch[2] });
|
|
235
265
|
}
|
|
266
|
+
|
|
236
267
|
const promptMatch = line.match(/^Prompt user to\s+"(.+)"$/i);
|
|
237
268
|
if (promptMatch) {
|
|
238
269
|
steps.push({ type: 'prompt', question: promptMatch[1], saveAs: null });
|
|
239
270
|
}
|
|
271
|
+
|
|
272
|
+
const useMatch = line.match(/^Use\s+(.+)$/i);
|
|
273
|
+
if (useMatch) {
|
|
274
|
+
steps.push({ type: 'use', tool: useMatch[1].trim(), saveAs: null, constraints: {} });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const askMatch = line.match(/^Ask\s+(.+)$/i);
|
|
278
|
+
if (askMatch) {
|
|
279
|
+
steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
|
|
280
|
+
}
|
|
240
281
|
}
|
|
241
282
|
return steps;
|
|
242
283
|
}
|
|
243
284
|
|
|
244
285
|
module.exports = { parse };
|
|
245
|
-
|
package/src/runtime.js
CHANGED
|
@@ -35,7 +35,6 @@ class RuntimeAPI {
|
|
|
35
35
|
return path.split('.').reduce((o, k) => (o && o[k] !== undefined) ? o[k] : undefined, obj);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// 🔍 Find the last "Ask ... Save as ..." step that can be evolved
|
|
39
38
|
findLastSummaryStep() {
|
|
40
39
|
for (let i = this.workflowSteps.length - 1; i >= 0; i--) {
|
|
41
40
|
const step = this.workflowSteps[i];
|
|
@@ -58,6 +57,20 @@ class RuntimeAPI {
|
|
|
58
57
|
break;
|
|
59
58
|
}
|
|
60
59
|
|
|
60
|
+
case 'use': {
|
|
61
|
+
// "Use <Tool>" step
|
|
62
|
+
const res = await agentResolver(`Use ${step.tool}`, this.context);
|
|
63
|
+
if (step.saveAs) this.context[step.saveAs] = res;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case 'ask': {
|
|
68
|
+
// "Ask <Target>" step
|
|
69
|
+
const res = await agentResolver(`Ask ${step.target}`, this.context);
|
|
70
|
+
if (step.saveAs) this.context[step.saveAs] = res;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
case 'if': {
|
|
62
75
|
if (this.evaluateCondition(step.condition, this.context)) {
|
|
63
76
|
for (const s of step.body) await this.executeStep(s, agentResolver);
|
|
@@ -86,7 +99,6 @@ class RuntimeAPI {
|
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
case 'evolve': {
|
|
89
|
-
// ✅ Runtime-enforced bounded evolution
|
|
90
102
|
const maxGen = step.constraints?.max_generations || 1;
|
|
91
103
|
if (maxGen < 1) {
|
|
92
104
|
this.context['improved_summary'] = this.context['summary'] || '';
|
|
@@ -100,34 +112,28 @@ class RuntimeAPI {
|
|
|
100
112
|
return;
|
|
101
113
|
}
|
|
102
114
|
|
|
103
|
-
const varName = summaryStep.saveAs;
|
|
115
|
+
const varName = summaryStep.saveAs;
|
|
104
116
|
let currentOutput = this.context[varName] || '';
|
|
105
117
|
|
|
106
|
-
// Run up to max_generations attempts
|
|
107
118
|
for (let attempt = 0; attempt < maxGen; attempt++) {
|
|
108
|
-
// Rebuild the original action with current context (re-interpolates {doc.text}, etc.)
|
|
109
119
|
let revisedAction = summaryStep.actionRaw.replace(/\{([^\}]+)\}/g, (_, path) => {
|
|
110
120
|
const val = this.getNested(this.context, path.trim());
|
|
111
121
|
return val !== undefined ? String(val) : `{${path}}`;
|
|
112
122
|
});
|
|
113
123
|
|
|
114
|
-
// Append improvement feedback on every attempt (including first)
|
|
115
124
|
if (step.feedback) {
|
|
116
|
-
revisedAction = revisedAction.replace(/(")
|
|
125
|
+
revisedAction = revisedAction.replace(/(")$/, `\n\n[IMPROVEMENT FEEDBACK: ${step.feedback}]$1`);
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
// Delegate ONLY the "Ask ..." action to the resolver
|
|
120
128
|
currentOutput = await agentResolver(revisedAction, this.context);
|
|
121
|
-
this.context[varName] = currentOutput;
|
|
129
|
+
this.context[varName] = currentOutput;
|
|
122
130
|
|
|
123
|
-
// Optional: emit debrief for observability
|
|
124
131
|
this.emit('debrief', {
|
|
125
132
|
agent: step.agent || 'Evolver',
|
|
126
133
|
message: `Evolve attempt ${attempt + 1}/${maxGen}: ${currentOutput.substring(0, 80)}...`
|
|
127
134
|
});
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
// ✅ Always expose final result as 'improved_summary' for downstream use
|
|
131
137
|
this.context['improved_summary'] = currentOutput;
|
|
132
138
|
break;
|
|
133
139
|
}
|
|
@@ -167,7 +173,7 @@ class RuntimeAPI {
|
|
|
167
173
|
|
|
168
174
|
async executeWorkflow(workflow, inputs, agentResolver) {
|
|
169
175
|
this.context = { ...inputs };
|
|
170
|
-
this.workflowSteps = workflow.steps;
|
|
176
|
+
this.workflowSteps = workflow.steps;
|
|
171
177
|
|
|
172
178
|
for (const step of workflow.steps) {
|
|
173
179
|
await this.executeStep(step, agentResolver);
|