@o-lang/olang 1.0.7 → 1.0.8

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.7",
3
+ "version": "1.0.8",
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,7 +1,7 @@
1
1
  // src/parser.js
2
2
 
3
3
  function parse(code, fileName = null) {
4
- // --- New: Enforce .ol extension if filename provided ---
4
+ // --- Enforce .ol extension if filename provided ---
5
5
  if (fileName && !fileName.endsWith(".ol")) {
6
6
  throw new Error(`Expected .ol workflow, got: ${fileName}`);
7
7
  }
@@ -15,18 +15,30 @@ function parse(code, fileName = null) {
15
15
  name: 'Unnamed Workflow',
16
16
  parameters: [],
17
17
  steps: [],
18
- returnValues: []
18
+ returnValues: [],
19
+ allowedResolvers: [] // NEW: store allowed resolvers
19
20
  };
20
21
 
21
22
  let i = 0;
22
23
  while (i < lines.length) {
23
24
  let line = lines[i];
24
25
 
26
+ // ---------------------------
27
+ // NEW: Detect Allow resolvers
28
+ // ---------------------------
29
+ const allowMatch = line.match(/^Allow resolvers\s*:\s*$/i);
30
+ if (allowMatch) {
31
+ i++;
32
+ while (i < lines.length && lines[i].startsWith(' ')) {
33
+ workflow.allowedResolvers.push(lines[i].trim());
34
+ i++;
35
+ }
36
+ continue;
37
+ }
38
+
25
39
  // ============================
26
- // NEW: Detect math operations..
40
+ // Math operations
27
41
  // ============================
28
-
29
- // Add X and Y
30
42
  let mathAdd = line.match(/^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
31
43
  if (mathAdd) {
32
44
  workflow.steps.push({
@@ -38,7 +50,6 @@ function parse(code, fileName = null) {
38
50
  continue;
39
51
  }
40
52
 
41
- // Subtract A from B => B - A
42
53
  let mathSub = line.match(/^Subtract\s+\{(.+?)\}\s+from\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
43
54
  if (mathSub) {
44
55
  workflow.steps.push({
@@ -50,7 +61,6 @@ function parse(code, fileName = null) {
50
61
  continue;
51
62
  }
52
63
 
53
- // Multiply X and Y
54
64
  let mathMul = line.match(/^Multiply\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
55
65
  if (mathMul) {
56
66
  workflow.steps.push({
@@ -62,7 +72,6 @@ function parse(code, fileName = null) {
62
72
  continue;
63
73
  }
64
74
 
65
- // Divide A by B
66
75
  let mathDiv = line.match(/^Divide\s+\{(.+?)\}\s+by\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
67
76
  if (mathDiv) {
68
77
  workflow.steps.push({
@@ -74,11 +83,9 @@ function parse(code, fileName = null) {
74
83
  continue;
75
84
  }
76
85
 
77
- // ====================== END NEW MATH RULES ======================
78
-
79
-
80
-
81
- // Workflow
86
+ // ---------------------------
87
+ // Workflow definition
88
+ // ---------------------------
82
89
  const wfMatch = line.match(/^Workflow\s+"([^"]+)"(?:\s+with\s+(.+))?/i);
83
90
  if (wfMatch) {
84
91
  workflow.name = wfMatch[1];
@@ -87,7 +94,9 @@ function parse(code, fileName = null) {
87
94
  continue;
88
95
  }
89
96
 
90
- // Step
97
+ // ---------------------------
98
+ // Steps
99
+ // ---------------------------
91
100
  const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
92
101
  if (stepMatch) {
93
102
  workflow.steps.push({
@@ -101,7 +110,6 @@ function parse(code, fileName = null) {
101
110
  continue;
102
111
  }
103
112
 
104
- // Save as
105
113
  const saveMatch = line.match(/^Save as\s+(.+)$/i);
106
114
  if (saveMatch && workflow.steps.length > 0) {
107
115
  workflow.steps[workflow.steps.length - 1].saveAs = saveMatch[1].trim();
@@ -109,7 +117,6 @@ function parse(code, fileName = null) {
109
117
  continue;
110
118
  }
111
119
 
112
- // Constraint
113
120
  const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
114
121
  if (constraintMatch && workflow.steps.length > 0) {
115
122
  const lastStep = workflow.steps[workflow.steps.length - 1];
@@ -135,7 +142,9 @@ function parse(code, fileName = null) {
135
142
  continue;
136
143
  }
137
144
 
138
- // If
145
+ // ---------------------------
146
+ // If blocks
147
+ // ---------------------------
139
148
  const ifMatch = line.match(/^If\s+(.+)\s+then$/i);
140
149
  if (ifMatch) {
141
150
  const condition = ifMatch[1].trim();
@@ -150,7 +159,9 @@ function parse(code, fileName = null) {
150
159
  continue;
151
160
  }
152
161
 
153
- // Parallel
162
+ // ---------------------------
163
+ // Parallel blocks
164
+ // ---------------------------
154
165
  const parMatch = line.match(/^Run in parallel$/i);
155
166
  if (parMatch) {
156
167
  const steps = [];
@@ -164,7 +175,9 @@ function parse(code, fileName = null) {
164
175
  continue;
165
176
  }
166
177
 
178
+ // ---------------------------
167
179
  // Connect
180
+ // ---------------------------
168
181
  const connMatch = line.match(/^Connect\s+"([^"]+)"\s+using\s+"([^"]+)"$/i);
169
182
  if (connMatch) {
170
183
  workflow.steps.push({
@@ -176,7 +189,9 @@ function parse(code, fileName = null) {
176
189
  continue;
177
190
  }
178
191
 
192
+ // ---------------------------
179
193
  // Agent uses
194
+ // ---------------------------
180
195
  const agentUseMatch = line.match(/^Agent\s+"([^"]+)"\s+uses\s+"([^"]+)"$/i);
181
196
  if (agentUseMatch) {
182
197
  workflow.steps.push({
@@ -188,7 +203,9 @@ function parse(code, fileName = null) {
188
203
  continue;
189
204
  }
190
205
 
206
+ // ---------------------------
191
207
  // Debrief
208
+ // ---------------------------
192
209
  const debriefMatch = line.match(/^Debrief\s+(\w+)\s+with\s+"(.+)"$/i);
193
210
  if (debriefMatch) {
194
211
  workflow.steps.push({
@@ -200,7 +217,9 @@ function parse(code, fileName = null) {
200
217
  continue;
201
218
  }
202
219
 
220
+ // ---------------------------
203
221
  // Evolve
222
+ // ---------------------------
204
223
  const evolveMatch = line.match(/^Evolve\s+(\w+)\s+using\s+feedback:\s+"(.+)"$/i);
205
224
  if (evolveMatch) {
206
225
  workflow.steps.push({
@@ -212,7 +231,9 @@ function parse(code, fileName = null) {
212
231
  continue;
213
232
  }
214
233
 
215
- // Prompt
234
+ // ---------------------------
235
+ // Prompt user
236
+ // ---------------------------
216
237
  const promptMatch = line.match(/^Prompt user to\s+"(.+)"$/i);
217
238
  if (promptMatch) {
218
239
  workflow.steps.push({
@@ -224,7 +245,9 @@ function parse(code, fileName = null) {
224
245
  continue;
225
246
  }
226
247
 
248
+ // ---------------------------
227
249
  // Persist
250
+ // ---------------------------
228
251
  const persistMatch = line.match(/^Persist\s+(.+)\s+to\s+"(.+)"$/i);
229
252
  if (persistMatch) {
230
253
  workflow.steps.push({
@@ -236,7 +259,9 @@ function parse(code, fileName = null) {
236
259
  continue;
237
260
  }
238
261
 
262
+ // ---------------------------
239
263
  // Emit
264
+ // ---------------------------
240
265
  const emitMatch = line.match(/^Emit\s+"(.+)"\s+with\s+(.+)$/i);
241
266
  if (emitMatch) {
242
267
  workflow.steps.push({
@@ -248,7 +273,9 @@ function parse(code, fileName = null) {
248
273
  continue;
249
274
  }
250
275
 
276
+ // ---------------------------
251
277
  // Return
278
+ // ---------------------------
252
279
  const returnMatch = line.match(/^Return\s+(.+)$/i);
253
280
  if (returnMatch) {
254
281
  workflow.returnValues = returnMatch[1].split(',').map(v => v.trim());
@@ -256,7 +283,9 @@ function parse(code, fileName = null) {
256
283
  continue;
257
284
  }
258
285
 
259
- // Use <Tool>
286
+ // ---------------------------
287
+ // Use tool
288
+ // ---------------------------
260
289
  const useMatch = line.match(/^Use\s+(.+)$/i);
261
290
  if (useMatch) {
262
291
  workflow.steps.push({
@@ -269,7 +298,9 @@ function parse(code, fileName = null) {
269
298
  continue;
270
299
  }
271
300
 
272
- // Ask <Target>
301
+ // ---------------------------
302
+ // Ask target
303
+ // ---------------------------
273
304
  const askMatch = line.match(/^Ask\s+(.+)$/i);
274
305
  if (askMatch) {
275
306
  workflow.steps.push({
@@ -288,6 +319,9 @@ function parse(code, fileName = null) {
288
319
  return workflow;
289
320
  }
290
321
 
322
+ // ---------------------------
323
+ // Parse nested blocks (If / Parallel)
324
+ // ---------------------------
291
325
  function parseBlock(lines) {
292
326
  const steps = [];
293
327
  let current = null;
package/src/runtime.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const fs = require('fs');
2
+ const path = require('path');
2
3
 
3
4
  class RuntimeAPI {
4
5
  constructor({ verbose = false } = {}) {
@@ -6,10 +7,22 @@ class RuntimeAPI {
6
7
  this.resources = {};
7
8
  this.agentMap = {};
8
9
  this.events = {};
9
- this.workflowSteps = []; // store for evolve lookup
10
- this.verbose = verbose; // verbose logging flag
10
+ this.workflowSteps = [];
11
+ this.allowedResolvers = null;
12
+ this.verbose = verbose;
13
+
14
+ // Ensure logs folder exists
15
+ const logsDir = path.resolve('./logs');
16
+ if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
17
+ this.disallowedLogFile = path.join(logsDir, 'disallowed_resolvers.json');
18
+
19
+ // In-memory store for summary
20
+ this.disallowedAttempts = [];
11
21
  }
12
22
 
23
+ // -----------------------------
24
+ // Event handling
25
+ // -----------------------------
13
26
  on(eventName, cb) {
14
27
  if (!this.events[eventName]) this.events[eventName] = [];
15
28
  this.events[eventName].push(cb);
@@ -21,6 +34,40 @@ class RuntimeAPI {
21
34
  }
22
35
  }
23
36
 
37
+ // -----------------------------
38
+ // Disallowed resolver handling
39
+ // -----------------------------
40
+ logDisallowedResolver(resolverName, stepAction) {
41
+ const entry = {
42
+ resolver: resolverName,
43
+ step: stepAction,
44
+ timestamp: new Date().toISOString()
45
+ };
46
+ fs.appendFileSync(this.disallowedLogFile, JSON.stringify(entry) + '\n', 'utf8');
47
+ this.disallowedAttempts.push(entry);
48
+
49
+ if (this.verbose) {
50
+ console.warn(`[O-Lang] Disallowed resolver blocked: ${resolverName} | step: ${stepAction}`);
51
+ }
52
+ }
53
+
54
+ printDisallowedSummary() {
55
+ if (!this.disallowedAttempts.length) return;
56
+ console.log('\n[O-Lang] ⚠️ Disallowed resolver summary:');
57
+ console.log(`Total blocked attempts: ${this.disallowedAttempts.length}`);
58
+ const displayCount = Math.min(5, this.disallowedAttempts.length);
59
+ console.log(`First ${displayCount} entries:`);
60
+ this.disallowedAttempts.slice(0, displayCount).forEach((e, i) => {
61
+ console.log(`${i + 1}. Resolver: ${e.resolver}, Step: ${e.step}, Time: ${e.timestamp}`);
62
+ });
63
+ if (this.disallowedAttempts.length > displayCount) {
64
+ console.log(`...and ${this.disallowedAttempts.length - displayCount} more entries logged in ${this.disallowedLogFile}`);
65
+ }
66
+ }
67
+
68
+ // -----------------------------
69
+ // Condition & math utilities
70
+ // -----------------------------
24
71
  evaluateCondition(cond, ctx) {
25
72
  cond = cond.trim();
26
73
  const eq = cond.match(/^\{(.+)\}\s+equals\s+"(.*)"$/);
@@ -37,16 +84,6 @@ class RuntimeAPI {
37
84
  return path.split('.').reduce((o, k) => (o && o[k] !== undefined) ? o[k] : undefined, obj);
38
85
  }
39
86
 
40
- findLastSummaryStep() {
41
- for (let i = this.workflowSteps.length - 1; i >= 0; i--) {
42
- const step = this.workflowSteps[i];
43
- if (step.type === 'action' && step.actionRaw?.startsWith('Ask ') && step.saveAs) {
44
- return step;
45
- }
46
- }
47
- return null;
48
- }
49
-
50
87
  mathFunctions = {
51
88
  add: (a, b) => a + b,
52
89
  subtract: (a, b) => a - b,
@@ -76,9 +113,7 @@ class RuntimeAPI {
76
113
 
77
114
  const funcNames = Object.keys(this.mathFunctions);
78
115
  const safeFunc = {};
79
- funcNames.forEach(fn => {
80
- safeFunc[fn] = this.mathFunctions[fn];
81
- });
116
+ funcNames.forEach(fn => safeFunc[fn] = this.mathFunctions[fn]);
82
117
 
83
118
  try {
84
119
  const f = new Function(...funcNames, `return ${expr};`);
@@ -89,18 +124,37 @@ class RuntimeAPI {
89
124
  }
90
125
  }
91
126
 
92
- // --------------------------
93
- // Execute workflow step
94
- // --------------------------
127
+ findLastSummaryStep() {
128
+ for (let i = this.workflowSteps.length - 1; i >= 0; i--) {
129
+ const step = this.workflowSteps[i];
130
+ if (step.type === 'action' && step.actionRaw?.startsWith('Ask ') && step.saveAs) {
131
+ return step;
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+
137
+ // -----------------------------
138
+ // Step execution
139
+ // -----------------------------
95
140
  async executeStep(step, agentResolver) {
96
141
  const stepType = step.type;
97
142
 
98
- // Helper: execute all resolvers for this step action
143
+ const validateResolver = (resolver) => {
144
+ const resolverName = resolver?.name || resolver?.resolverName;
145
+ if (!resolverName) throw new Error('[O-Lang] Resolver missing name metadata');
146
+ if (!this.allowedResolvers.has(resolverName)) {
147
+ this.logDisallowedResolver(resolverName, step.actionRaw || step.tool || step.target);
148
+ throw new Error(`[O-Lang] Resolver "${resolverName}" is not allowed by workflow policy`);
149
+ }
150
+ };
151
+
99
152
  const runAllResolvers = async (action) => {
100
153
  const outputs = [];
101
154
  if (agentResolver && Array.isArray(agentResolver._chain)) {
102
155
  for (let idx = 0; idx < agentResolver._chain.length; idx++) {
103
156
  const resolver = agentResolver._chain[idx];
157
+ validateResolver(resolver);
104
158
  try {
105
159
  const out = await resolver(action, this.context);
106
160
  outputs.push(out);
@@ -111,11 +165,12 @@ class RuntimeAPI {
111
165
  }
112
166
  }
113
167
  } else {
168
+ validateResolver(agentResolver);
114
169
  const out = await agentResolver(action, this.context);
115
170
  outputs.push(out);
116
171
  this.context['__resolver_0'] = out;
117
172
  }
118
- return outputs[outputs.length - 1]; // last result as primary
173
+ return outputs[outputs.length - 1];
119
174
  };
120
175
 
121
176
  switch (stepType) {
@@ -125,13 +180,11 @@ class RuntimeAPI {
125
180
  if (step.saveAs) this.context[step.saveAs] = result;
126
181
  break;
127
182
  }
128
-
129
183
  case 'action': {
130
184
  const action = step.actionRaw.replace(/\{([^\}]+)\}/g, (_, path) => {
131
185
  const value = this.getNested(this.context, path.trim());
132
186
  return value !== undefined ? String(value) : `{${path}}`;
133
187
  });
134
-
135
188
  const mathCall = action.match(/^(add|subtract|multiply|divide|sum|avg|min|max|round|floor|ceil|abs)\((.*)\)$/i);
136
189
  if (mathCall) {
137
190
  const fn = mathCall[1].toLowerCase();
@@ -148,97 +201,79 @@ class RuntimeAPI {
148
201
  break;
149
202
  }
150
203
  }
151
-
152
204
  const res = await runAllResolvers(action);
153
205
  if (step.saveAs) this.context[step.saveAs] = res;
154
206
  break;
155
207
  }
156
-
157
208
  case 'use': {
158
209
  const res = await runAllResolvers(`Use ${step.tool}`);
159
210
  if (step.saveAs) this.context[step.saveAs] = res;
160
211
  break;
161
212
  }
162
-
163
213
  case 'ask': {
164
214
  const res = await runAllResolvers(`Ask ${step.target}`);
165
215
  if (step.saveAs) this.context[step.saveAs] = res;
166
216
  break;
167
217
  }
168
-
169
218
  case 'if': {
170
219
  if (this.evaluateCondition(step.condition, this.context)) {
171
220
  for (const s of step.body) await this.executeStep(s, agentResolver);
172
221
  }
173
222
  break;
174
223
  }
175
-
176
224
  case 'parallel': {
177
225
  await Promise.all(step.steps.map(s => this.executeStep(s, agentResolver)));
178
226
  break;
179
227
  }
180
-
181
228
  case 'connect': {
182
229
  this.resources[step.resource] = step.endpoint;
183
230
  break;
184
231
  }
185
-
186
232
  case 'agent_use': {
187
233
  this.agentMap[step.logicalName] = step.resource;
188
234
  break;
189
235
  }
190
-
191
236
  case 'debrief': {
192
237
  this.emit('debrief', { agent: step.agent, message: step.message });
193
238
  break;
194
239
  }
195
-
196
240
  case 'evolve': {
197
241
  const maxGen = step.constraints?.max_generations || 1;
198
242
  if (maxGen < 1) {
199
243
  this.context['improved_summary'] = this.context['summary'] || '';
200
244
  return;
201
245
  }
202
-
203
246
  const summaryStep = this.findLastSummaryStep();
204
247
  if (!summaryStep) {
205
248
  console.warn('[O-Lang] Evolve: No prior "Ask ... Save as" step found to evolve');
206
249
  this.context['improved_summary'] = this.context['summary'] || '';
207
250
  return;
208
251
  }
209
-
210
252
  const varName = summaryStep.saveAs;
211
253
  let currentOutput = this.context[varName] || '';
212
-
213
254
  for (let attempt = 0; attempt < maxGen; attempt++) {
214
255
  let revisedAction = summaryStep.actionRaw.replace(/\{([^\}]+)\}/g, (_, path) => {
215
256
  const val = this.getNested(this.context, path.trim());
216
257
  return val !== undefined ? String(val) : `{${path}}`;
217
258
  });
218
-
219
259
  if (step.feedback) {
220
260
  revisedAction = revisedAction.replace(/(")$/, `\n\n[IMPROVEMENT FEEDBACK: ${step.feedback}]$1`);
221
261
  }
222
-
223
262
  currentOutput = await runAllResolvers(revisedAction);
224
263
  this.context[varName] = currentOutput;
225
-
226
264
  this.emit('debrief', {
227
265
  agent: step.agent || 'Evolver',
228
266
  message: `Evolve attempt ${attempt + 1}/${maxGen}: ${currentOutput.substring(0, 80)}...`
229
267
  });
230
268
  }
231
-
232
269
  this.context['improved_summary'] = currentOutput;
233
270
  break;
234
271
  }
235
-
236
272
  case 'prompt': {
237
273
  const input = await this.getUserInput(step.question);
238
274
  if (step.saveAs) this.context[step.saveAs] = input;
239
275
  break;
240
276
  }
241
-
242
277
  case 'persist': {
243
278
  const val = this.context[step.variable];
244
279
  if (val !== undefined) {
@@ -246,7 +281,6 @@ class RuntimeAPI {
246
281
  }
247
282
  break;
248
283
  }
249
-
250
284
  case 'emit': {
251
285
  const payload = step.payload ? this.getNested(this.context, step.payload) : undefined;
252
286
  this.emit(step.event, payload || step.payload);
@@ -254,7 +288,6 @@ class RuntimeAPI {
254
288
  }
255
289
  }
256
290
 
257
- // Verbose logging of context after each step
258
291
  if (this.verbose) {
259
292
  console.log(`\n[Step: ${step.type} | saveAs: ${step.saveAs || 'N/A'}]`);
260
293
  console.log(JSON.stringify(this.context, null, 2));
@@ -275,11 +308,15 @@ class RuntimeAPI {
275
308
  async executeWorkflow(workflow, inputs, agentResolver) {
276
309
  this.context = { ...inputs };
277
310
  this.workflowSteps = workflow.steps;
311
+ this.allowedResolvers = new Set(workflow.allowedResolvers || []);
278
312
 
279
313
  for (const step of workflow.steps) {
280
314
  await this.executeStep(step, agentResolver);
281
315
  }
282
316
 
317
+ // Print disallowed resolver summary at the end
318
+ this.printDisallowedSummary();
319
+
283
320
  const result = {};
284
321
  for (const key of workflow.returnValues) {
285
322
  result[key] = this.getNested(this.context, key);