@o-lang/olang 1.0.6 → 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/cli.js CHANGED
@@ -49,61 +49,47 @@ async function defaultMockResolver(action, context) {
49
49
  }
50
50
 
51
51
  /**
52
- * Built-in Math Resolver (so action style math strings are handled too)
53
- * Supports action strings like: add(1,2), subtract(5,2), multiply(2,3), divide(6,3), sum([1,2,3])
54
- * Note: runtime handles calculate steps; this resolver helps when parser emits actions with math strings.
52
+ * Built-in Math Resolver
55
53
  */
56
54
  async function builtInMathResolver(action, context) {
57
55
  if (!action || typeof action !== 'string') return null;
58
56
 
59
- // Replace contextual placeholders {var}
60
57
  const a = action.replace(/\{([^\}]+)\}/g, (_, k) => {
61
58
  const v = context[k.trim()];
62
59
  return v !== undefined ? v : `{${k}}`;
63
60
  });
64
61
 
65
- // simple function matches
66
62
  let m;
67
- m = a.match(/^add\(([^,]+),\s*([^)]+)\)$/i);
68
- if (m) return parseFloat(m[1]) + parseFloat(m[2]);
63
+ m = a.match(/^add\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) + parseFloat(m[2]);
64
+ m = a.match(/^subtract\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) - parseFloat(m[2]);
65
+ m = a.match(/^multiply\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) * parseFloat(m[2]);
66
+ m = a.match(/^divide\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) / parseFloat(m[2]);
67
+ m = a.match(/^sum\(\s*\[([^\]]+)\]\s*\)$/i); if (m) return m[1].split(',').map(s => parseFloat(s.trim())).reduce((s, v) => s + v, 0);
69
68
 
70
- m = a.match(/^subtract\(([^,]+),\s*([^)]+)\)$/i);
71
- if (m) return parseFloat(m[1]) - parseFloat(m[2]);
72
-
73
- m = a.match(/^multiply\(([^,]+),\s*([^)]+)\)$/i);
74
- if (m) return parseFloat(m[1]) * parseFloat(m[2]);
75
-
76
- m = a.match(/^divide\(([^,]+),\s*([^)]+)\)$/i);
77
- if (m) return parseFloat(m[1]) / parseFloat(m[2]);
78
-
79
- m = a.match(/^sum\(\s*\[([^\]]+)\]\s*\)$/i);
80
- if (m) {
81
- const arr = m[1].split(',').map(s => parseFloat(s.trim()));
82
- return arr.reduce((s, v) => s + v, 0);
83
- }
84
-
85
- // not a math action
86
69
  return null;
87
70
  }
88
71
 
89
72
  /**
90
- * Resolver chaining mechanism
73
+ * Resolver chaining with verbose + context logging
91
74
  */
92
- function createResolverChain(resolvers) {
93
- return async (action, context) => {
94
- for (const resolver of resolvers) {
75
+ function createResolverChain(resolvers, verbose = false) {
76
+ const chain = resolvers.slice();
77
+ const wrapped = async (action, context) => {
78
+ let lastResult;
79
+ for (let i = 0; i < chain.length; i++) {
95
80
  try {
96
- const result = await resolver(action, context);
97
- if (result !== null && result !== undefined) {
98
- return result;
99
- }
100
- } catch (err) {
101
- console.error(`❌ Resolver error for action "${action}":`, err.message);
102
- throw err;
81
+ const res = await chain[i](action, context);
82
+ context[`__resolver_${i}`] = res;
83
+ lastResult = res;
84
+ } catch (e) {
85
+ console.error(`❌ Resolver ${i} failed for action "${action}":`, e.message);
103
86
  }
104
87
  }
105
- return `[Unhandled: ${action}]`;
88
+ if (verbose) console.log(`[Resolver Chain] action="${action}" lastResult=`, lastResult);
89
+ return lastResult;
106
90
  };
91
+ wrapped._chain = chain;
92
+ return wrapped;
107
93
  }
108
94
 
109
95
  function loadSingleResolver(specifier) {
@@ -111,9 +97,7 @@ function loadSingleResolver(specifier) {
111
97
 
112
98
  try {
113
99
  const resolver = require(specifier);
114
- if (typeof resolver !== 'function') {
115
- throw new Error(`Resolver must export a function`);
116
- }
100
+ if (typeof resolver !== 'function') throw new Error(`Resolver must export a function`);
117
101
  console.log(`📦 Loaded resolver: ${specifier}`);
118
102
  return resolver;
119
103
  } catch (e1) {
@@ -133,7 +117,7 @@ function loadSingleResolver(specifier) {
133
117
  /**
134
118
  * loadResolverChain: include built-in math resolver first, then user resolvers, then default mock resolver
135
119
  */
136
- function loadResolverChain(specifiers) {
120
+ function loadResolverChain(specifiers, verbose = false) {
137
121
  const userResolvers = specifiers?.map(loadSingleResolver) || [];
138
122
  const resolvers = [builtInMathResolver, ...userResolvers, defaultMockResolver];
139
123
 
@@ -143,7 +127,7 @@ function loadResolverChain(specifiers) {
143
127
  console.log(`📦 Loaded user resolvers: ${specifiers.join(', ')}`);
144
128
  }
145
129
 
146
- return createResolverChain(resolvers);
130
+ return createResolverChain(resolvers, verbose);
147
131
  }
148
132
 
149
133
  /**
@@ -157,7 +141,7 @@ program
157
141
  .command('run <file>')
158
142
  .option(
159
143
  '-r, --resolver <specifier>',
160
- 'Resolver (npm package or local path). Can be used multiple times.\nExample:\n -r @o-lang/llm-groq\n -r @o-lang/notify-telegram',
144
+ 'Resolver (npm package or local path). Can be used multiple times.',
161
145
  (val, acc) => { acc.push(val); return acc; },
162
146
  []
163
147
  )
@@ -166,13 +150,13 @@ program
166
150
  'Input parameters',
167
151
  (val, acc = {}) => {
168
152
  const [k, v] = val.split('=');
169
- // try to parse numbers, preserve strings otherwise
170
153
  const parsed = v === undefined ? '' : (v === 'true' ? true : (v === 'false' ? false : (isNaN(v) ? v : parseFloat(v))));
171
154
  acc[k] = parsed;
172
155
  return acc;
173
156
  },
174
157
  {}
175
158
  )
159
+ .option('-v, --verbose', 'Verbose mode: logs resolver outputs and context after each step')
176
160
  .action(async (file, options) => {
177
161
  try {
178
162
  ensureOlExtension(file);
@@ -180,8 +164,8 @@ program
180
164
  const content = fs.readFileSync(file, 'utf8');
181
165
  const workflow = parse(content);
182
166
 
183
- const resolver = loadResolverChain(options.resolver);
184
- const result = await execute(workflow, options.input, resolver);
167
+ const resolver = loadResolverChain(options.resolver, options.verbose);
168
+ const result = await execute(workflow, options.input, resolver, options.verbose);
185
169
 
186
170
  console.log('\n=== Workflow Result ===');
187
171
  console.log(JSON.stringify(result, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.0.6",
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,15 +1,28 @@
1
- // src/runtime.js
2
1
  const fs = require('fs');
2
+ const path = require('path');
3
3
 
4
4
  class RuntimeAPI {
5
- constructor() {
5
+ constructor({ verbose = false } = {}) {
6
6
  this.context = {};
7
7
  this.resources = {};
8
8
  this.agentMap = {};
9
9
  this.events = {};
10
- this.workflowSteps = []; // store for evolve lookup
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,19 +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
- // --------------------------
51
- // Math helper functions
52
- // --------------------------
53
87
  mathFunctions = {
54
88
  add: (a, b) => a + b,
55
89
  subtract: (a, b) => a - b,
@@ -71,23 +105,17 @@ class RuntimeAPI {
71
105
  };
72
106
 
73
107
  evaluateMath(expr) {
74
- // Replace context variables in curly braces
75
108
  expr = expr.replace(/\{([^\}]+)\}/g, (_, path) => {
76
109
  const value = this.getNested(this.context, path.trim());
77
- // if value is string, wrap in quotes for functions that accept strings
78
110
  if (typeof value === 'string') return `"${value.replace(/"/g, '\\"')}"`;
79
111
  return value !== undefined ? value : 0;
80
112
  });
81
113
 
82
- // expose safe functions only
83
114
  const funcNames = Object.keys(this.mathFunctions);
84
115
  const safeFunc = {};
85
- funcNames.forEach(fn => {
86
- safeFunc[fn] = this.mathFunctions[fn];
87
- });
116
+ funcNames.forEach(fn => safeFunc[fn] = this.mathFunctions[fn]);
88
117
 
89
118
  try {
90
- // eslint-disable-next-line no-new-func
91
119
  const f = new Function(...funcNames, `return ${expr};`);
92
120
  return f(...funcNames.map(fn => safeFunc[fn]));
93
121
  } catch (e) {
@@ -96,140 +124,156 @@ class RuntimeAPI {
96
124
  }
97
125
  }
98
126
 
99
- // --------------------------
100
- // Execute workflow step
101
- // --------------------------
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
+ // -----------------------------
102
140
  async executeStep(step, agentResolver) {
103
- switch (step.type) {
141
+ const stepType = step.type;
142
+
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
+
152
+ const runAllResolvers = async (action) => {
153
+ const outputs = [];
154
+ if (agentResolver && Array.isArray(agentResolver._chain)) {
155
+ for (let idx = 0; idx < agentResolver._chain.length; idx++) {
156
+ const resolver = agentResolver._chain[idx];
157
+ validateResolver(resolver);
158
+ try {
159
+ const out = await resolver(action, this.context);
160
+ outputs.push(out);
161
+ this.context[`__resolver_${idx}`] = out;
162
+ } catch (e) {
163
+ console.error(`❌ Resolver ${idx} error for action "${action}":`, e.message);
164
+ outputs.push(null);
165
+ }
166
+ }
167
+ } else {
168
+ validateResolver(agentResolver);
169
+ const out = await agentResolver(action, this.context);
170
+ outputs.push(out);
171
+ this.context['__resolver_0'] = out;
172
+ }
173
+ return outputs[outputs.length - 1];
174
+ };
175
+
176
+ switch (stepType) {
104
177
  case 'calculate': {
105
- // step.expression or actionRaw can contain math expression strings
106
178
  const expr = step.expression || step.actionRaw;
107
179
  const result = this.evaluateMath(expr);
108
180
  if (step.saveAs) this.context[step.saveAs] = result;
109
181
  break;
110
182
  }
111
-
112
183
  case 'action': {
113
184
  const action = step.actionRaw.replace(/\{([^\}]+)\}/g, (_, path) => {
114
185
  const value = this.getNested(this.context, path.trim());
115
186
  return value !== undefined ? String(value) : `{${path}}`;
116
187
  });
117
-
118
- // Provide fallback math recognition for action lines too (e.g., add(1,2))
119
- // Try simple math function calls in action form
120
188
  const mathCall = action.match(/^(add|subtract|multiply|divide|sum|avg|min|max|round|floor|ceil|abs)\((.*)\)$/i);
121
189
  if (mathCall) {
122
190
  const fn = mathCall[1].toLowerCase();
123
191
  const argsRaw = mathCall[2];
124
- // simple split by comma (doesn't handle nested arrays/funcs) — fine for workflow math
125
192
  const args = argsRaw.split(',').map(s => s.trim()).map(s => {
126
- // if it's a quoted string
127
193
  if (/^".*"$/.test(s) || /^'.*'$/.test(s)) return s.slice(1, -1);
128
- // try number
129
194
  if (!isNaN(s)) return parseFloat(s);
130
- // variable lookup
131
195
  const lookup = s.replace(/^\{|\}$/g, '').trim();
132
196
  return this.getNested(this.context, lookup);
133
197
  });
134
-
135
198
  if (this.mathFunctions[fn]) {
136
199
  const value = this.mathFunctions[fn](...args);
137
200
  if (step.saveAs) this.context[step.saveAs] = value;
138
201
  break;
139
202
  }
140
203
  }
141
-
142
- // fallback to agent resolver
143
- const res = await agentResolver(action, this.context);
204
+ const res = await runAllResolvers(action);
144
205
  if (step.saveAs) this.context[step.saveAs] = res;
145
206
  break;
146
207
  }
147
-
148
208
  case 'use': {
149
- const res = await agentResolver(`Use ${step.tool}`, this.context);
209
+ const res = await runAllResolvers(`Use ${step.tool}`);
150
210
  if (step.saveAs) this.context[step.saveAs] = res;
151
211
  break;
152
212
  }
153
-
154
213
  case 'ask': {
155
- const res = await agentResolver(`Ask ${step.target}`, this.context);
214
+ const res = await runAllResolvers(`Ask ${step.target}`);
156
215
  if (step.saveAs) this.context[step.saveAs] = res;
157
216
  break;
158
217
  }
159
-
160
218
  case 'if': {
161
219
  if (this.evaluateCondition(step.condition, this.context)) {
162
220
  for (const s of step.body) await this.executeStep(s, agentResolver);
163
221
  }
164
222
  break;
165
223
  }
166
-
167
224
  case 'parallel': {
168
225
  await Promise.all(step.steps.map(s => this.executeStep(s, agentResolver)));
169
226
  break;
170
227
  }
171
-
172
228
  case 'connect': {
173
229
  this.resources[step.resource] = step.endpoint;
174
230
  break;
175
231
  }
176
-
177
232
  case 'agent_use': {
178
233
  this.agentMap[step.logicalName] = step.resource;
179
234
  break;
180
235
  }
181
-
182
236
  case 'debrief': {
183
237
  this.emit('debrief', { agent: step.agent, message: step.message });
184
238
  break;
185
239
  }
186
-
187
240
  case 'evolve': {
188
241
  const maxGen = step.constraints?.max_generations || 1;
189
242
  if (maxGen < 1) {
190
243
  this.context['improved_summary'] = this.context['summary'] || '';
191
244
  return;
192
245
  }
193
-
194
246
  const summaryStep = this.findLastSummaryStep();
195
247
  if (!summaryStep) {
196
248
  console.warn('[O-Lang] Evolve: No prior "Ask ... Save as" step found to evolve');
197
249
  this.context['improved_summary'] = this.context['summary'] || '';
198
250
  return;
199
251
  }
200
-
201
252
  const varName = summaryStep.saveAs;
202
253
  let currentOutput = this.context[varName] || '';
203
-
204
254
  for (let attempt = 0; attempt < maxGen; attempt++) {
205
255
  let revisedAction = summaryStep.actionRaw.replace(/\{([^\}]+)\}/g, (_, path) => {
206
256
  const val = this.getNested(this.context, path.trim());
207
257
  return val !== undefined ? String(val) : `{${path}}`;
208
258
  });
209
-
210
259
  if (step.feedback) {
211
260
  revisedAction = revisedAction.replace(/(")$/, `\n\n[IMPROVEMENT FEEDBACK: ${step.feedback}]$1`);
212
261
  }
213
-
214
- currentOutput = await agentResolver(revisedAction, this.context);
262
+ currentOutput = await runAllResolvers(revisedAction);
215
263
  this.context[varName] = currentOutput;
216
-
217
264
  this.emit('debrief', {
218
265
  agent: step.agent || 'Evolver',
219
266
  message: `Evolve attempt ${attempt + 1}/${maxGen}: ${currentOutput.substring(0, 80)}...`
220
267
  });
221
268
  }
222
-
223
269
  this.context['improved_summary'] = currentOutput;
224
270
  break;
225
271
  }
226
-
227
272
  case 'prompt': {
228
273
  const input = await this.getUserInput(step.question);
229
274
  if (step.saveAs) this.context[step.saveAs] = input;
230
275
  break;
231
276
  }
232
-
233
277
  case 'persist': {
234
278
  const val = this.context[step.variable];
235
279
  if (val !== undefined) {
@@ -237,13 +281,17 @@ class RuntimeAPI {
237
281
  }
238
282
  break;
239
283
  }
240
-
241
284
  case 'emit': {
242
285
  const payload = step.payload ? this.getNested(this.context, step.payload) : undefined;
243
286
  this.emit(step.event, payload || step.payload);
244
287
  break;
245
288
  }
246
289
  }
290
+
291
+ if (this.verbose) {
292
+ console.log(`\n[Step: ${step.type} | saveAs: ${step.saveAs || 'N/A'}]`);
293
+ console.log(JSON.stringify(this.context, null, 2));
294
+ }
247
295
  }
248
296
 
249
297
  async getUserInput(question) {
@@ -260,11 +308,15 @@ class RuntimeAPI {
260
308
  async executeWorkflow(workflow, inputs, agentResolver) {
261
309
  this.context = { ...inputs };
262
310
  this.workflowSteps = workflow.steps;
311
+ this.allowedResolvers = new Set(workflow.allowedResolvers || []);
263
312
 
264
313
  for (const step of workflow.steps) {
265
314
  await this.executeStep(step, agentResolver);
266
315
  }
267
316
 
317
+ // Print disallowed resolver summary at the end
318
+ this.printDisallowedSummary();
319
+
268
320
  const result = {};
269
321
  for (const key of workflow.returnValues) {
270
322
  result[key] = this.getNested(this.context, key);
@@ -273,8 +325,8 @@ class RuntimeAPI {
273
325
  }
274
326
  }
275
327
 
276
- async function execute(workflow, inputs, agentResolver) {
277
- const rt = new RuntimeAPI();
328
+ async function execute(workflow, inputs, agentResolver, verbose = false) {
329
+ const rt = new RuntimeAPI({ verbose });
278
330
  return rt.executeWorkflow(workflow, inputs, agentResolver);
279
331
  }
280
332