@o-lang/olang 1.0.23 → 1.0.24

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.23",
3
+ "version": "1.0.24",
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,319 +1,255 @@
1
- // src/parser.js
2
- function parse(code, fileName = null) {
3
- if (fileName && !fileName.endsWith(".ol")) {
4
- throw new Error(`Expected .ol workflow, got: ${fileName}`);
1
+ const fs = require('fs');
2
+
3
+ function parse(content, filename = '<unknown>') {
4
+ if (typeof content === 'string') {
5
+ const lines = content.split('\n').map(line => line.replace(/\r$/, ''));
6
+ return parseLines(lines, filename);
7
+ } else if (typeof content === 'object' && content !== null) {
8
+ // Already parsed
9
+ return content;
10
+ } else {
11
+ throw new Error('parse() expects string content or pre-parsed object');
5
12
  }
6
- const rawLines = code.split(/\r?\n/);
7
- const lines = rawLines
8
- .map(l => l.trim())
9
- .filter(l => l && !l.startsWith('#') && !l.startsWith('//'));
13
+ }
14
+
15
+ function parseFromFile(filepath) {
16
+ const content = fs.readFileSync(filepath, 'utf8');
17
+ return parse(content, filepath);
18
+ }
10
19
 
20
+ function parseLines(lines, filename) {
21
+ // Remove evolution file parsing - evolution is now in-workflow
22
+ return parseWorkflowLines(lines, filename);
23
+ }
24
+
25
+ function parseWorkflowLines(lines, filename) {
11
26
  const workflow = {
12
- name: 'Unnamed Workflow',
27
+ type: 'workflow',
28
+ name: null,
13
29
  parameters: [],
14
30
  steps: [],
15
31
  returnValues: [],
16
32
  allowedResolvers: [],
17
- resolverPolicy: {
18
- declared: [],
19
- autoInjected: [],
20
- used: [],
21
- warnings: []
22
- },
33
+ maxGenerations: null, // ✅ Updated field name for Constraint: max_generations = X
23
34
  __warnings: [],
24
- __requiresMath: false
35
+ filename: filename
25
36
  };
26
-
37
+
27
38
  let i = 0;
39
+ let currentStep = null;
40
+ let inAllowResolvers = false;
41
+ let inIfBlock = false;
42
+ let ifCondition = null;
43
+ let ifBody = [];
44
+
28
45
  while (i < lines.length) {
29
- let line = lines[i];
30
-
31
- // FIXED: Match both "Allow resolvers" and "Allowed resolvers"
32
- const allowMatch = line.match(/^Allow resolvers\s*:\s*$/i);
33
- if (allowMatch) {
34
- i++;
35
- while (i < lines.length) {
36
- const nextLine = lines[i].trim();
37
- // Stop if line is empty or looks like a new top-level section
38
- if (nextLine === '' || /^[A-Z][a-z]/.test(nextLine)) {
39
- break;
40
- }
41
- // Strip optional YAML list marker: "- name" → "name"
42
- const cleanVal = nextLine.replace(/^\-\s*/, '').trim();
43
- if (cleanVal) {
44
- workflow.allowedResolvers.push(cleanVal);
45
- workflow.resolverPolicy.declared.push(cleanVal);
46
- }
47
- i++;
48
- }
46
+ let line = lines[i++].trim();
47
+
48
+ // Skip empty lines and comments
49
+ if (line === '' || line.startsWith('#')) {
49
50
  continue;
50
51
  }
51
-
52
- // ✅ NEW: Parse database Persist steps
53
- const dbPersistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+db:([^\s]+)$/i);
54
- if (dbPersistMatch) {
55
- workflow.steps.push({
56
- type: 'persist-db',
57
- source: dbPersistMatch[1].trim(),
58
- collection: dbPersistMatch[2].trim()
59
- });
60
- i++;
52
+
53
+ // Parse Workflow declaration
54
+ if (line.startsWith('Workflow ')) {
55
+ const match = line.match(/^Workflow\s+"([^"]+)"(?:\s+with\s+(.+))?$/i);
56
+ if (match) {
57
+ workflow.name = match[1];
58
+ if (match[2]) {
59
+ workflow.parameters = match[2].split(',').map(p => p.trim()).filter(p => p !== '');
60
+ }
61
+ } else {
62
+ workflow.__warnings.push(`Invalid Workflow syntax: ${line}`);
63
+ }
61
64
  continue;
62
65
  }
63
-
64
- // Parse file Persist steps
65
- const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]+)"$/i);
66
- if (persistMatch) {
67
- workflow.steps.push({
68
- type: 'persist',
69
- source: persistMatch[1].trim(),
70
- destination: persistMatch[2].trim()
71
- });
72
- i++;
66
+
67
+ // Parse Constraint: max_generations = X (✅ Updated syntax)
68
+ if (line.startsWith('Constraint: max_generations = ')) {
69
+ const match = line.match(/^Constraint:\s+max_generations\s*=\s*(\d+)$/i);
70
+ if (match) {
71
+ workflow.maxGenerations = parseInt(match[1], 10);
72
+ } else {
73
+ workflow.__warnings.push(`Invalid Constraint syntax: ${line}`);
74
+ }
73
75
  continue;
74
76
  }
75
-
76
- // ---- Math step patterns (standalone) ----
77
- let mathAdd = line.match(/^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
78
- if (mathAdd) {
79
- workflow.__requiresMath = true;
80
- workflow.steps.push({
81
- type: 'calculate',
82
- expression: `add({${mathAdd[1]}}, {${mathAdd[2]}})`,
83
- saveAs: mathAdd[3].trim()
84
- });
85
- i++;
77
+
78
+ // Parse Allow resolvers section
79
+ if (line === 'Allow resolvers:') {
80
+ inAllowResolvers = true;
86
81
  continue;
87
82
  }
88
-
89
- let mathSub = line.match(/^Subtract\s+\{(.+?)\}\s+from\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
90
- if (mathSub) {
91
- workflow.__requiresMath = true;
92
- workflow.steps.push({
93
- type: 'calculate',
94
- expression: `subtract({${mathSub[2]}}, {${mathSub[1]}})`, // Fixed: was mathAdd
95
- saveAs: mathSub[3].trim()
96
- });
97
- i++;
83
+
84
+ if (inAllowResolvers) {
85
+ if (line.startsWith('- ')) {
86
+ const resolverName = line.substring(2).trim();
87
+ if (resolverName) {
88
+ workflow.allowedResolvers.push(resolverName);
89
+ }
90
+ } else if (line === '' || line.startsWith('#')) {
91
+ // Continue
92
+ } else {
93
+ // End of Allow resolvers section
94
+ inAllowResolvers = false;
95
+ i--; // Re-process this line
96
+ continue;
97
+ }
98
98
  continue;
99
99
  }
100
-
101
- let mathMul = line.match(/^Multiply\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
102
- if (mathMul) {
103
- workflow.__requiresMath = true;
104
- workflow.steps.push({
105
- type: 'calculate',
106
- expression: `multiply({${mathMul[1]}}, {${mathMul[2]}})`,
107
- saveAs: mathMul[3].trim()
108
- });
109
- i++;
100
+
101
+ // Parse Step declarations
102
+ const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
103
+ if (stepMatch) {
104
+ // Save previous step if it exists
105
+ if (currentStep) {
106
+ workflow.steps.push(currentStep);
107
+ currentStep = null;
108
+ }
109
+
110
+ const stepNumber = parseInt(stepMatch[1], 10);
111
+ const stepContent = stepMatch[2];
112
+
113
+ currentStep = {
114
+ type: 'action',
115
+ stepNumber: stepNumber,
116
+ actionRaw: stepContent,
117
+ saveAs: null,
118
+ constraints: {}
119
+ };
110
120
  continue;
111
121
  }
112
-
113
- let mathDiv = line.match(/^Divide\s+\{(.+?)\}\s+by\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
114
- if (mathDiv) {
115
- workflow.__requiresMath = true;
116
- workflow.steps.push({
117
- type: 'calculate',
118
- expression: `divide({${mathDiv[1]}}, {${mathDiv[2]}})`,
119
- saveAs: mathDiv[3].trim()
120
- });
121
- i++;
122
+
123
+ // Parse Evolve steps ( NEW IN-WORKFLOW EVOLUTION)
124
+ const evolveMatch = line.match(/^Evolve\s+([^\s]+)\s+using\s+feedback:\s*"([^"]*)"$/i);
125
+ if (evolveMatch) {
126
+ if (currentStep) {
127
+ workflow.steps.push(currentStep);
128
+ currentStep = null;
129
+ }
130
+
131
+ currentStep = {
132
+ type: 'evolve',
133
+ stepNumber: workflow.steps.length + 1,
134
+ targetResolver: evolveMatch[1].trim(),
135
+ feedback: evolveMatch[2],
136
+ saveAs: null,
137
+ constraints: {}
138
+ };
122
139
  continue;
123
140
  }
124
-
125
- // ---- Workflow declaration ----
126
- const wfMatch = line.match(/^Workflow\s+"([^"]+)"(?:\s+with\s+(.+))?/i);
127
- if (wfMatch) {
128
- workflow.name = wfMatch[1];
129
- workflow.parameters = wfMatch[2]
130
- ? wfMatch[2].split(',').map(p => p.trim())
131
- : [];
132
- i++;
141
+
142
+ // Parse Save as
143
+ const saveMatch = line.match(/^Save as\s+(.+)$/i);
144
+ if (saveMatch && currentStep) {
145
+ currentStep.saveAs = saveMatch[1].trim();
133
146
  continue;
134
147
  }
135
-
136
- // ---- Return statement (updated: auto-detect math) ----
137
- const returnMatch = line.match(/^Return\s+(.+)$/i);
138
- if (returnMatch) {
139
- const returns = returnMatch[1].split(',').map(v => v.trim());
140
- workflow.returnValues = returns;
141
- // Check if any return vars come from math steps
142
- for (const retVar of returns) {
143
- const producedByMath = workflow.steps.some(
144
- s => s.saveAs === retVar && s.type === 'calculate'
145
- );
146
- if (producedByMath) workflow.__requiresMath = true;
147
- }
148
- i++;
148
+
149
+ // Parse If/When conditions
150
+ const ifMatch = line.match(/^(?:If|When)\s+(.+)$/i);
151
+ if (ifMatch) {
152
+ ifCondition = ifMatch[1].trim();
153
+ inIfBlock = true;
154
+ ifBody = [];
149
155
  continue;
150
156
  }
151
-
152
- // ---- Step parsing (with inline math detection) ----
153
- const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
154
- if (stepMatch) {
155
- const stepNum = parseInt(stepMatch[1], 10);
156
- const raw = stepMatch[2].trim();
157
-
158
- let mathDetected = null;
159
- let expr = '';
160
- let saveVar = null;
161
- const mathOps = [
162
- { re: /^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'add' },
163
- { re: /^Subtract\s+\{(.+?)\}\s+from\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'subtract' },
164
- { re: /^Multiply\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'multiply' },
165
- { re: /^Divide\s+\{(.+?)\}\s+by\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'divide' }
166
- ];
167
- for (const op of mathOps) {
168
- const m = raw.match(op.re);
169
- if (m) {
170
- mathDetected = op.fn;
171
- saveVar = m[3].trim();
172
- if (op.fn === 'subtract') {
173
- expr = `subtract({${m[2]}}, {${m[1]}})`;
174
- } else {
175
- expr = `${op.fn}({${m[1]}}, {${m[2]}})`;
176
- }
177
- break;
178
- }
157
+
158
+ const endIfMatch = line.match(/^End(?:If)?$/i);
159
+ if (endIfMatch && inIfBlock) {
160
+ if (currentStep) {
161
+ workflow.steps.push(currentStep);
162
+ currentStep = null;
179
163
  }
180
- if (mathDetected) workflow.__requiresMath = true;
164
+
181
165
  workflow.steps.push({
182
- type: mathDetected ? 'calculate' : 'action',
183
- stepNumber: stepNum,
184
- actionRaw: mathDetected ? null : raw,
185
- expression: mathDetected ? expr : undefined,
186
- saveAs: saveVar,
187
- constraints: {}
166
+ type: 'if',
167
+ condition: ifCondition,
168
+ body: ifBody,
169
+ stepNumber: workflow.steps.length + 1
188
170
  });
189
- i++;
171
+
172
+ inIfBlock = false;
173
+ ifCondition = null;
174
+ ifBody = [];
190
175
  continue;
191
176
  }
192
-
193
- // ---- Save as (for legacy or multi-line steps) ----
194
- const saveMatch = line.match(/^Save as\s+(.+)$/i);
195
- if (saveMatch && workflow.steps.length > 0) {
196
- const lastStep = workflow.steps[workflow.steps.length - 1];
197
- lastStep.saveAs = saveMatch[1].trim();
198
- if (lastStep.saveAs.match(/[A-Z][A-Za-z0-9_]*/)) {
199
- workflow.__requiresMath = true;
200
- }
201
- i++;
177
+
178
+ // Handle lines inside If block
179
+ if (inIfBlock) {
180
+ ifBody.push(line);
202
181
  continue;
203
182
  }
204
-
205
- // ---- Constraint parsing ----
206
- const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
207
- if (constraintMatch && workflow.steps.length > 0) {
208
- const lastStep = workflow.steps[workflow.steps.length - 1];
209
- if (!lastStep.constraints) lastStep.constraints = {};
210
- const eq = constraintMatch[1].match(/^([^=]+)=\s*(.+)$/);
211
- if (eq) {
212
- let key = eq[1].trim();
213
- let value = eq[2].trim();
214
- if (value.startsWith('[') && value.endsWith(']')) {
215
- value = value.slice(1, -1).split(',').map(v =>
216
- v.trim().replace(/^"/, '').replace(/"$/, '')
217
- );
218
- } else if (!isNaN(value)) {
219
- value = Number(value);
220
- } else if (value.startsWith('"') && value.endsWith('"')) {
221
- value = value.slice(1, -1);
222
- }
223
- lastStep.constraints[key] = value;
183
+
184
+ // Parse Return statement
185
+ const returnMatch = line.match(/^Return\s+(.+)$/i);
186
+ if (returnMatch) {
187
+ if (currentStep) {
188
+ workflow.steps.push(currentStep);
189
+ currentStep = null;
224
190
  }
225
- i++;
191
+ workflow.returnValues = returnMatch[1].split(',').map(r => r.trim()).filter(r => r !== '');
226
192
  continue;
227
193
  }
228
-
229
- i++;
194
+
195
+ // If we reach here and have unprocessed content, it's likely a workflow line without "Step X:"
196
+ // Try to handle it as a step
197
+ if (line.trim() !== '') {
198
+ if (!currentStep) {
199
+ currentStep = {
200
+ type: 'action',
201
+ stepNumber: workflow.steps.length + 1,
202
+ actionRaw: line,
203
+ saveAs: null,
204
+ constraints: {}
205
+ };
206
+ } else {
207
+ // Append to current step action (multi-line)
208
+ currentStep.actionRaw += ' ' + line;
209
+ }
210
+ }
230
211
  }
231
-
232
- // ============================
233
- // LINT & POLICY FINALIZATION
234
- // ============================
235
- if (workflow.__requiresMath) {
236
- workflow.resolverPolicy.used.push('builtInMathResolver');
237
- if (!workflow.resolverPolicy.declared.includes('builtInMathResolver')) {
238
- workflow.resolverPolicy.autoInjected.push('builtInMathResolver');
239
- workflow.allowedResolvers.unshift('builtInMathResolver');
240
- workflow.__warnings.push(
241
- 'Math operations detected. builtInMathResolver auto-injected.'
242
- );
212
+
213
+ // Don't forget the last step
214
+ if (currentStep) {
215
+ workflow.steps.push(currentStep);
216
+ }
217
+
218
+ // Post-process steps to extract Save as from actionRaw
219
+ workflow.steps.forEach(step => {
220
+ if (step.actionRaw && step.saveAs === null) {
221
+ const saveInAction = step.actionRaw.match(/(.+?)\s+Save as\s+(.+)$/i);
222
+ if (saveInAction) {
223
+ step.actionRaw = saveInAction[1].trim();
224
+ step.saveAs = saveInAction[2].trim();
225
+ }
243
226
  }
227
+ });
228
+
229
+ // Check for common issues
230
+ if (!workflow.name) {
231
+ workflow.__warnings.push('Workflow name not found');
244
232
  }
245
-
246
- if (workflow.resolverPolicy.declared.length === 0) {
247
- workflow.__warnings.push(
248
- 'No "Allow resolvers" section declared. Workflow will run in restricted mode.'
249
- );
233
+
234
+ if (workflow.steps.length === 0) {
235
+ workflow.__warnings.push('No steps found in workflow');
250
236
  }
251
-
252
- workflow.resolverPolicy.warnings = workflow.__warnings.slice();
237
+
238
+ if (workflow.returnValues.length === 0 && workflow.steps.length > 0) {
239
+ workflow.__warnings.push('No Return statement found');
240
+ }
241
+
253
242
  return workflow;
254
243
  }
255
244
 
256
- // ---------------------------
257
- // Parse nested blocks (updated)
258
- // ---------------------------
259
- function parseBlock(lines) {
260
- const steps = [];
261
- let current = null;
262
- for (const line of lines) {
263
- const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
264
- if (stepMatch) {
265
- current = {
266
- type: 'action',
267
- stepNumber: parseInt(stepMatch[1], 10),
268
- actionRaw: stepMatch[2].trim(),
269
- saveAs: null,
270
- constraints: {}
271
- };
272
- steps.push(current);
273
- continue;
274
- }
275
- const saveMatch = line.match(/^Save as\s+(.+)$/i);
276
- if (saveMatch && current) current.saveAs = saveMatch[1].trim();
277
- const debriefMatch = line.match(/^Debrief\s+(\w+)\s+with\s+"(.+)"$/i);
278
- if (debriefMatch) {
279
- steps.push({ type: 'debrief', agent: debriefMatch[1], message: debriefMatch[2] });
280
- }
281
- const evolveMatch = line.match(/^Evolve\s+(\w+)\s+using\s+feedback:\s+"(.+)"$/i);
282
- if (evolveMatch) {
283
- steps.push({ type: 'evolve', agent: evolveMatch[1], feedback: evolveMatch[2] });
284
- }
285
- const promptMatch = line.match(/^Prompt user to\s+"(.+)"$/i);
286
- if (promptMatch) {
287
- steps.push({ type: 'prompt', question: promptMatch[1], saveAs: null });
288
- }
289
- const useMatch = line.match(/^Use\s+(.+)$/i);
290
- if (useMatch) {
291
- steps.push({ type: 'use', tool: useMatch[1].trim(), saveAs: null, constraints: {} });
292
- }
293
- const askMatch = line.match(/^Ask\s+(.+)$/i);
294
- if (askMatch) {
295
- steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
296
- }
297
- // ✅ Parse file Persist in blocks
298
- const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]+)"$/i);
299
- if (persistMatch) {
300
- steps.push({
301
- type: 'persist',
302
- source: persistMatch[1].trim(),
303
- destination: persistMatch[2].trim()
304
- });
305
- }
306
- // ✅ NEW: Parse database Persist in blocks
307
- const dbPersistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+db:([^\s]+)$/i);
308
- if (dbPersistMatch) {
309
- steps.push({
310
- type: 'persist-db',
311
- source: dbPersistMatch[1].trim(),
312
- collection: dbPersistMatch[2].trim()
313
- });
314
- }
245
+ function validate(workflow) {
246
+ const errors = [];
247
+
248
+ if (workflow.maxGenerations !== null && workflow.maxGenerations <= 0) {
249
+ errors.push('max_generations must be positive');
315
250
  }
316
- return steps;
251
+
252
+ return errors;
317
253
  }
318
254
 
319
- module.exports = { parse };
255
+ module.exports = { parse, parseFromFile, parseLines, validate };
package/src/runtime.js CHANGED
@@ -328,6 +328,35 @@ class RuntimeAPI {
328
328
  break;
329
329
  }
330
330
 
331
+ case 'evolve': {
332
+ // ✅ Handle in-workflow Evolve steps
333
+ const { targetResolver, feedback } = step;
334
+
335
+ if (this.verbose) {
336
+ console.log(`🔄 Evolve step: ${targetResolver} with feedback: "${feedback}"`);
337
+ }
338
+
339
+ // Basic evolution: record the request (free tier)
340
+ const evolutionResult = {
341
+ resolver: targetResolver,
342
+ feedback: feedback,
343
+ status: 'evolution_requested',
344
+ timestamp: new Date().toISOString(),
345
+ workflow: this.context.workflow_name
346
+ };
347
+
348
+ // ✅ Check for Advanced Evolution Service (paid tier)
349
+ if (process.env.OLANG_EVOLUTION_API_KEY) {
350
+ evolutionResult.status = 'advanced_evolution_enabled';
351
+ evolution resultList.message = 'Advanced evolution service would process this request';
352
+ }
353
+
354
+ if (step.saveAs) {
355
+ this.context[step.saveAs] = evolutionResult;
356
+ }
357
+ break;
358
+ }
359
+
331
360
  case 'if': {
332
361
  if (this.evaluateCondition(step.condition, this.context)) {
333
362
  for (const s of step.body) await this.executeStep(s, agentResolver);
@@ -452,11 +481,23 @@ class RuntimeAPI {
452
481
  }
453
482
 
454
483
  async executeWorkflow(workflow, inputs, agentResolver) {
484
+ // Handle regular workflows only (Evolve is a step type now)
485
+ if (workflow.type !== 'workflow') {
486
+ throw new Error(`Unknown workflow type: ${workflow.type}`);
487
+ }
488
+
455
489
  // ✅ Inject workflow name into context
456
490
  this.context = {
457
491
  ...inputs,
458
492
  workflow_name: workflow.name
459
493
  };
494
+
495
+ // ✅ Check generation constraint from Constraint: max_generations = X
496
+ const currentGeneration = inputs.__generation || 1;
497
+ if (workflow.maxGenerations !== null && currentGeneration > workflow.maxGenerations) {
498
+ throw new Error(`Workflow generation ${currentGeneration} exceeds Constraint: max_generations = ${workflow.maxGenerations}`);
499
+ }
500
+
460
501
  this.workflowSteps = workflow.steps;
461
502
  this.allowedResolvers = new Set(workflow.allowedResolvers || []);
462
503