@o-lang/olang 1.0.14 → 1.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
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",
package/src/parser.js CHANGED
@@ -1,12 +1,9 @@
1
1
  // src/parser.js
2
-
3
2
  function parse(code, fileName = null) {
4
3
  if (fileName && !fileName.endsWith(".ol")) {
5
4
  throw new Error(`Expected .ol workflow, got: ${fileName}`);
6
5
  }
7
-
8
6
  const rawLines = code.split(/\r?\n/);
9
-
10
7
  const lines = rawLines
11
8
  .map(l => l.trim())
12
9
  .filter(l => l && !l.startsWith('#') && !l.startsWith('//'));
@@ -17,37 +14,41 @@ function parse(code, fileName = null) {
17
14
  steps: [],
18
15
  returnValues: [],
19
16
  allowedResolvers: [],
20
-
21
17
  resolverPolicy: {
22
18
  declared: [],
23
19
  autoInjected: [],
24
20
  used: [],
25
21
  warnings: []
26
22
  },
27
-
28
23
  __warnings: [],
29
24
  __requiresMath: false
30
25
  };
31
26
 
32
27
  let i = 0;
33
-
34
28
  while (i < lines.length) {
35
29
  let line = lines[i];
36
30
 
37
- const allowMatch = line.match(/^Allow resolvers\s*:\s*$/i);
31
+ // FIXED: Match both "Allow resolvers" and "Allowed resolvers"
32
+ const allowMatch = line.match(/^Allow(?:ed)?\s+resolvers\s*:\s*$/i);
38
33
  if (allowMatch) {
39
34
  i++;
40
- while (i < lines.length && !/^[A-Za-z]/.test(lines[i])) {
41
- const val = lines[i].trim();
42
- if (val) {
43
- workflow.allowedResolvers.push(val);
44
- workflow.resolverPolicy.declared.push(val);
35
+ // Read all subsequent non-empty lines until a new top-level section (starts with capital letter)
36
+ while (i < lines.length) {
37
+ const nextLine = lines[i].trim();
38
+ // Stop if line is empty or appears to be a new top-level keyword (e.g., Step, Workflow, Return)
39
+ if (nextLine === '' || /^[A-Z][a-z]/.test(nextLine)) {
40
+ break;
41
+ }
42
+ if (nextLine) {
43
+ workflow.allowedResolvers.push(nextLine);
44
+ workflow.resolverPolicy.declared.push(nextLine);
45
45
  }
46
46
  i++;
47
47
  }
48
48
  continue;
49
49
  }
50
50
 
51
+ // ---- Math step patterns (standalone) ----
51
52
  let mathAdd = line.match(/^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
52
53
  if (mathAdd) {
53
54
  workflow.__requiresMath = true;
@@ -65,7 +66,7 @@ function parse(code, fileName = null) {
65
66
  workflow.__requiresMath = true;
66
67
  workflow.steps.push({
67
68
  type: 'calculate',
68
- expression: `subtract({${mathSub[2]}}, {${mathSub[1]}})`,
69
+ expression: `subtract({${mathAdd[2]}}, {${mathAdd[1]}})`,
69
70
  saveAs: mathSub[3].trim()
70
71
  });
71
72
  i++;
@@ -96,6 +97,7 @@ function parse(code, fileName = null) {
96
97
  continue;
97
98
  }
98
99
 
100
+ // ---- Workflow declaration ----
99
101
  const wfMatch = line.match(/^Workflow\s+"([^"]+)"(?:\s+with\s+(.+))?/i);
100
102
  if (wfMatch) {
101
103
  workflow.name = wfMatch[1];
@@ -106,96 +108,84 @@ function parse(code, fileName = null) {
106
108
  continue;
107
109
  }
108
110
 
109
- // ---------------------------
110
- // Return statement (updated: auto-detect math)
111
- // ---------------------------
112
- const returnMatch = line.match(/^Return\s+(.+)$/i);
113
- if (returnMatch) {
114
- const returns = returnMatch[1].split(',').map(v => v.trim());
115
- workflow.returnValues = returns;
116
-
117
- // --- Check if any return vars come from math steps ---
118
- for (const retVar of returns) {
119
- const producedByMath = workflow.steps.some(
120
- s => s.saveAs === retVar && s.type === 'calculate'
121
- );
122
- if (producedByMath) workflow.__requiresMath = true;
123
- }
124
-
125
- i++;
126
- continue;
127
- }
128
-
129
-
130
- // ---------------------------
131
- // Steps (updated: auto-detect math + saveAs)
132
- // ---------------------------
133
- const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
134
- if (stepMatch) {
135
- const stepNum = parseInt(stepMatch[1], 10);
136
- const raw = stepMatch[2].trim();
137
-
138
- // --- Detect math inside Step ---
139
- let mathDetected = null;
140
- let expr = '';
141
- let saveVar = null;
142
-
143
- const mathOps = [
144
- { re: /^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'add' },
145
- { re: /^Subtract\s+\{(.+?)\}\s+from\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'subtract' },
146
- { re: /^Multiply\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'multiply' },
147
- { re: /^Divide\s+\{(.+?)\}\s+by\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'divide' }
148
- ];
149
-
150
- for (const op of mathOps) {
151
- const m = raw.match(op.re);
152
- if (m) {
153
- mathDetected = op.fn;
154
- saveVar = m[3].trim();
155
- if (op.fn === 'subtract') expr = `subtract({${m[2]}}, {${m[1]}})`;
156
- else expr = `${op.fn}({${m[1]}}, {${m[2]}})`;
157
- break;
111
+ // ---- Return statement (updated: auto-detect math) ----
112
+ const returnMatch = line.match(/^Return\s+(.+)$/i);
113
+ if (returnMatch) {
114
+ const returns = returnMatch[1].split(',').map(v => v.trim());
115
+ workflow.returnValues = returns;
116
+ // Check if any return vars come from math steps
117
+ for (const retVar of returns) {
118
+ const producedByMath = workflow.steps.some(
119
+ s => s.saveAs === retVar && s.type === 'calculate'
120
+ );
121
+ if (producedByMath) workflow.__requiresMath = true;
122
+ }
123
+ i++;
124
+ continue;
158
125
  }
159
- }
160
-
161
- if (mathDetected) workflow.__requiresMath = true;
162
-
163
- workflow.steps.push({
164
- type: mathDetected ? 'calculate' : 'action',
165
- stepNumber: stepNum,
166
- actionRaw: mathDetected ? null : raw,
167
- expression: mathDetected ? expr : undefined,
168
- saveAs: saveVar,
169
- constraints: {}
170
- });
171
126
 
172
- i++;
173
- continue;
174
- }
127
+ // ---- Step parsing (with inline math detection) ----
128
+ const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
129
+ if (stepMatch) {
130
+ const stepNum = parseInt(stepMatch[1], 10);
131
+ const raw = stepMatch[2].trim();
132
+
133
+ let mathDetected = null;
134
+ let expr = '';
135
+ let saveVar = null;
136
+ const mathOps = [
137
+ { re: /^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'add' },
138
+ { re: /^Subtract\s+\{(.+?)\}\s+from\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'subtract' },
139
+ { re: /^Multiply\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'multiply' },
140
+ { re: /^Divide\s+\{(.+?)\}\s+by\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'divide' }
141
+ ];
142
+ for (const op of mathOps) {
143
+ const m = raw.match(op.re);
144
+ if (m) {
145
+ mathDetected = op.fn;
146
+ saveVar = m[3].trim();
147
+ if (op.fn === 'subtract') {
148
+ expr = `subtract({${m[2]}}, {${m[1]}})`;
149
+ } else {
150
+ expr = `${op.fn}({${m[1]}}, {${m[2]}})`;
151
+ }
152
+ break;
153
+ }
154
+ }
155
+ if (mathDetected) workflow.__requiresMath = true;
156
+ workflow.steps.push({
157
+ type: mathDetected ? 'calculate' : 'action',
158
+ stepNumber: stepNum,
159
+ actionRaw: mathDetected ? null : raw,
160
+ expression: mathDetected ? expr : undefined,
161
+ saveAs: saveVar,
162
+ constraints: {}
163
+ });
164
+ i++;
165
+ continue;
166
+ }
175
167
 
168
+ // ---- Save as (for legacy or multi-line steps) ----
176
169
  const saveMatch = line.match(/^Save as\s+(.+)$/i);
177
170
  if (saveMatch && workflow.steps.length > 0) {
178
171
  const lastStep = workflow.steps[workflow.steps.length - 1];
179
172
  lastStep.saveAs = saveMatch[1].trim();
180
-
181
173
  if (lastStep.saveAs.match(/[A-Z][A-Za-z0-9_]*/)) {
182
174
  workflow.__requiresMath = true;
183
175
  }
184
-
185
176
  i++;
186
177
  continue;
187
178
  }
188
179
 
180
+ // ---- Constraint parsing ----
189
181
  const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
190
182
  if (constraintMatch && workflow.steps.length > 0) {
191
183
  const lastStep = workflow.steps[workflow.steps.length - 1];
192
184
  if (!lastStep.constraints) lastStep.constraints = {};
193
-
194
185
  const eq = constraintMatch[1].match(/^([^=]+)=\s*(.+)$/);
195
186
  if (eq) {
196
187
  let key = eq[1].trim();
197
188
  let value = eq[2].trim();
198
-
199
189
  if (value.startsWith('[') && value.endsWith(']')) {
200
190
  value = value.slice(1, -1).split(',').map(v =>
201
191
  v.trim().replace(/^"/, '').replace(/"$/, '')
@@ -205,7 +195,6 @@ if (stepMatch) {
205
195
  } else if (value.startsWith('"') && value.endsWith('"')) {
206
196
  value = value.slice(1, -1);
207
197
  }
208
-
209
198
  lastStep.constraints[key] = value;
210
199
  }
211
200
  i++;
@@ -220,7 +209,6 @@ if (stepMatch) {
220
209
  // ============================
221
210
  if (workflow.__requiresMath) {
222
211
  workflow.resolverPolicy.used.push('builtInMathResolver');
223
-
224
212
  if (!workflow.resolverPolicy.declared.includes('builtInMathResolver')) {
225
213
  workflow.resolverPolicy.autoInjected.push('builtInMathResolver');
226
214
  workflow.allowedResolvers.unshift('builtInMathResolver');
@@ -237,7 +225,6 @@ if (stepMatch) {
237
225
  }
238
226
 
239
227
  workflow.resolverPolicy.warnings = workflow.__warnings.slice();
240
-
241
228
  return workflow;
242
229
  }
243
230
 
@@ -247,7 +234,6 @@ if (stepMatch) {
247
234
  function parseBlock(lines) {
248
235
  const steps = [];
249
236
  let current = null;
250
-
251
237
  for (const line of lines) {
252
238
  const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
253
239
  if (stepMatch) {
@@ -261,37 +247,30 @@ function parseBlock(lines) {
261
247
  steps.push(current);
262
248
  continue;
263
249
  }
264
-
265
250
  const saveMatch = line.match(/^Save as\s+(.+)$/i);
266
251
  if (saveMatch && current) current.saveAs = saveMatch[1].trim();
267
-
268
252
  const debriefMatch = line.match(/^Debrief\s+(\w+)\s+with\s+"(.+)"$/i);
269
253
  if (debriefMatch) {
270
254
  steps.push({ type: 'debrief', agent: debriefMatch[1], message: debriefMatch[2] });
271
255
  }
272
-
273
256
  const evolveMatch = line.match(/^Evolve\s+(\w+)\s+using\s+feedback:\s+"(.+)"$/i);
274
257
  if (evolveMatch) {
275
258
  steps.push({ type: 'evolve', agent: evolveMatch[1], feedback: evolveMatch[2] });
276
259
  }
277
-
278
260
  const promptMatch = line.match(/^Prompt user to\s+"(.+)"$/i);
279
261
  if (promptMatch) {
280
262
  steps.push({ type: 'prompt', question: promptMatch[1], saveAs: null });
281
263
  }
282
-
283
264
  const useMatch = line.match(/^Use\s+(.+)$/i);
284
265
  if (useMatch) {
285
266
  steps.push({ type: 'use', tool: useMatch[1].trim(), saveAs: null, constraints: {} });
286
267
  }
287
-
288
268
  const askMatch = line.match(/^Ask\s+(.+)$/i);
289
269
  if (askMatch) {
290
270
  steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
291
271
  }
292
272
  }
293
-
294
273
  return steps;
295
274
  }
296
275
 
297
- module.exports = { parse };
276
+ module.exports = { parse };
package/src/runtime.js CHANGED
@@ -86,7 +86,7 @@ class RuntimeAPI {
86
86
  const gt = cond.match(/^\{(.+)\}\s+greater than\s+(\d+\.?\d*)$/);
87
87
  if (gt) return parseFloat(this.getNested(ctx, gt[1])) > parseFloat(gt[2]);
88
88
  const lt = cond.match(/^\{(.+)\}\s+less than\s+(\d+\.?\d*)$/);
89
- if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(lt[2]);
89
+ if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(gt[2]);
90
90
  return Boolean(this.getNested(ctx, cond.replace(/\{|\}/g, '')));
91
91
  }
92
92
 
@@ -144,31 +144,37 @@ class RuntimeAPI {
144
144
  async executeStep(step, agentResolver) {
145
145
  const stepType = step.type;
146
146
 
147
- const validateResolver = (resolver) => {
148
- const resolverName = (resolver?.resolverName || resolver?.name || '').trim();
149
- if (!resolverName) throw new Error('[O-Lang] Resolver missing name metadata');
147
+ const validateResolver = (resolver) => {
148
+ const resolverName = (resolver?.resolverName || resolver?.name || '').trim();
149
+ if (!resolverName) throw new Error('[O-Lang] Resolver missing name metadata');
150
150
 
151
- const allowed = Array.from(this.allowedResolvers || []).map(r => r.trim());
152
-
153
- // Auto-inject builtInMathResolver if step type is 'calculate' and resolver is math
154
- if (step.type === 'calculate' && resolverName === 'builtInMathResolver' && !allowed.includes('builtInMathResolver')) {
155
- this.allowedResolvers.add('builtInMathResolver');
156
- allowed.push('builtInMathResolver');
157
- }
158
-
159
- if (!allowed.includes(resolverName)) {
160
- this.logDisallowedResolver(resolverName, step.actionRaw || step.tool || step.target);
161
- throw new Error(`[O-Lang] Resolver "${resolverName}" is not allowed by workflow policy`);
162
- }
163
- };
151
+ const allowed = Array.from(this.allowedResolvers || []).map(r => r.trim());
164
152
 
153
+ if (!allowed.includes(resolverName)) {
154
+ this.logDisallowedResolver(resolverName, step.actionRaw || step.tool || step.target);
155
+ throw new Error(`[O-Lang] Resolver "${resolverName}" is not allowed by workflow policy`);
156
+ }
157
+ };
165
158
 
166
159
  const runResolvers = async (action) => {
167
160
  const outputs = [];
161
+
162
+ const mathPattern =
163
+ /^(Add|Subtract|Multiply|Divide|Sum|Avg|Min|Max|Round|Floor|Ceil|Abs)\b/i;
164
+
165
+ if (
166
+ step.actionRaw &&
167
+ mathPattern.test(step.actionRaw) &&
168
+ !this.allowedResolvers.has('builtInMathResolver')
169
+ ) {
170
+ this.allowedResolvers.add('builtInMathResolver');
171
+ }
172
+
168
173
  if (agentResolver && Array.isArray(agentResolver._chain)) {
169
174
  for (let idx = 0; idx < agentResolver._chain.length; idx++) {
170
175
  const resolver = agentResolver._chain[idx];
171
176
  validateResolver(resolver);
177
+
172
178
  try {
173
179
  const out = await resolver(action, this.context);
174
180
  outputs.push(out);
@@ -184,16 +190,17 @@ class RuntimeAPI {
184
190
  outputs.push(out);
185
191
  this.context['__resolver_0'] = out;
186
192
  }
193
+
187
194
  return outputs[outputs.length - 1];
188
195
  };
189
196
 
190
- // --- execute based on step.type ---
191
197
  switch (stepType) {
192
198
  case 'calculate': {
193
199
  const result = this.evaluateMath(step.expression || step.actionRaw);
194
200
  if (step.saveAs) this.context[step.saveAs] = result;
195
201
  break;
196
202
  }
203
+
197
204
  case 'action': {
198
205
  const action = step.actionRaw.replace(/\{([^\}]+)\}/g, (_, path) => {
199
206
  const value = this.getNested(this.context, path.trim());
@@ -203,11 +210,10 @@ class RuntimeAPI {
203
210
  const mathCall = action.match(/^(add|subtract|multiply|divide|sum|avg|min|max|round|floor|ceil|abs)\((.*)\)$/i);
204
211
  if (mathCall) {
205
212
  const fn = mathCall[1].toLowerCase();
206
- const argsRaw = mathCall[2];
207
- const args = argsRaw.split(',').map(s => s.trim()).map(s => {
208
- if (/^".*"$/.test(s) || /^'.*'$/.test(s)) return s.slice(1, -1);
213
+ const args = mathCall[2].split(',').map(s => {
214
+ s = s.trim();
209
215
  if (!isNaN(s)) return parseFloat(s);
210
- return this.getNested(this.context, s.replace(/^\{|\}$/g, '').trim());
216
+ return this.getNested(this.context, s.replace(/^\{|\}$/g, ''));
211
217
  });
212
218
  if (this.mathFunctions[fn]) {
213
219
  const value = this.mathFunctions[fn](...args);
@@ -220,79 +226,45 @@ class RuntimeAPI {
220
226
  if (step.saveAs) this.context[step.saveAs] = res;
221
227
  break;
222
228
  }
229
+
223
230
  case 'use': {
224
231
  const res = await runResolvers(`Use ${step.tool}`);
225
232
  if (step.saveAs) this.context[step.saveAs] = res;
226
233
  break;
227
234
  }
235
+
228
236
  case 'ask': {
229
237
  const res = await runResolvers(`Ask ${step.target}`);
230
238
  if (step.saveAs) this.context[step.saveAs] = res;
231
239
  break;
232
240
  }
241
+
233
242
  case 'if': {
234
243
  if (this.evaluateCondition(step.condition, this.context)) {
235
244
  for (const s of step.body) await this.executeStep(s, agentResolver);
236
245
  }
237
246
  break;
238
247
  }
248
+
239
249
  case 'parallel': {
240
250
  await Promise.all(step.steps.map(s => this.executeStep(s, agentResolver)));
241
251
  break;
242
252
  }
253
+
243
254
  case 'connect': {
244
255
  this.resources[step.resource] = step.endpoint;
245
256
  break;
246
257
  }
258
+
247
259
  case 'agent_use': {
248
260
  this.agentMap[step.logicalName] = step.resource;
249
261
  break;
250
262
  }
263
+
251
264
  case 'debrief': {
252
265
  this.emit('debrief', { agent: step.agent, message: step.message });
253
266
  break;
254
267
  }
255
- case 'evolve': {
256
- const maxGen = step.constraints?.max_generations || 1;
257
- if (maxGen < 1) return;
258
- const summaryStep = this.findLastSummaryStep();
259
- if (!summaryStep) {
260
- this.addWarning('Evolve step has no prior "Ask ... Save as" step to evolve');
261
- return;
262
- }
263
- const varName = summaryStep.saveAs;
264
- let currentOutput = this.context[varName] || '';
265
- for (let attempt = 0; attempt < maxGen; attempt++) {
266
- let revisedAction = summaryStep.actionRaw.replace(/\{([^\}]+)\}/g, (_, path) => {
267
- const val = this.getNested(this.context, path.trim());
268
- return val !== undefined ? String(val) : `{${path}}`;
269
- });
270
- if (step.feedback) revisedAction += `\n[IMPROVEMENT FEEDBACK: ${step.feedback}]`;
271
- currentOutput = await runResolvers(revisedAction);
272
- this.context[varName] = currentOutput;
273
- this.emit('debrief', {
274
- agent: step.agent || 'Evolver',
275
- message: `Evolve attempt ${attempt + 1}/${maxGen}: ${currentOutput.substring(0, 80)}...`
276
- });
277
- }
278
- this.context['improved_summary'] = currentOutput;
279
- break;
280
- }
281
- case 'prompt': {
282
- const input = await this.getUserInput(step.question);
283
- if (step.saveAs) this.context[step.saveAs] = input;
284
- break;
285
- }
286
- case 'persist': {
287
- const val = this.context[step.variable];
288
- if (val !== undefined) fs.appendFileSync(step.target, JSON.stringify(val) + '\n', 'utf8');
289
- break;
290
- }
291
- case 'emit': {
292
- const payload = step.payload ? this.getNested(this.context, step.payload) : undefined;
293
- this.emit(step.event, payload || step.payload);
294
- break;
295
- }
296
268
  }
297
269
 
298
270
  if (this.verbose) {
@@ -301,23 +273,23 @@ class RuntimeAPI {
301
273
  }
302
274
  }
303
275
 
304
- async getUserInput(question) {
305
- return new Promise(resolve => {
306
- process.stdout.write(`${question}: `);
307
- process.stdin.resume();
308
- process.stdin.once('data', data => {
309
- process.stdin.pause();
310
- resolve(data.toString().trim());
311
- });
312
- });
313
- }
314
-
315
276
  async executeWorkflow(workflow, inputs, agentResolver) {
316
277
  this.context = { ...inputs };
317
278
  this.workflowSteps = workflow.steps;
318
279
  this.allowedResolvers = new Set(workflow.allowedResolvers || []);
319
280
 
320
- for (const step of workflow.steps) await this.executeStep(step, agentResolver);
281
+ const mathPattern =
282
+ /^(Add|Subtract|Multiply|Divide|Sum|Avg|Min|Max|Round|Floor|Ceil|Abs)\b/i;
283
+
284
+ for (const step of workflow.steps) {
285
+ if (step.type === 'calculate' || (step.actionRaw && mathPattern.test(step.actionRaw))) {
286
+ this.allowedResolvers.add('builtInMathResolver');
287
+ }
288
+ }
289
+
290
+ for (const step of workflow.steps) {
291
+ await this.executeStep(step, agentResolver);
292
+ }
321
293
 
322
294
  this.printDisallowedSummary();
323
295