@o-lang/olang 1.0.21 → 1.0.23

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
@@ -64,25 +64,32 @@ async function builtInMathResolver(action, context) {
64
64
  builtInMathResolver.resolverName = 'builtInMathResolver';
65
65
 
66
66
  /**
67
- * Resolver chaining with verbose + context logging
67
+ * Resolver chaining: returns the FIRST resolver that returns a non-undefined result.
68
68
  */
69
69
  function createResolverChain(resolvers, verbose = false) {
70
- const chain = resolvers.slice();
71
70
  const wrapped = async (action, context) => {
72
- let lastResult;
73
- for (let i = 0; i < chain.length; i++) {
71
+ for (let i = 0; i < resolvers.length; i++) {
72
+ const resolver = resolvers[i];
74
73
  try {
75
- const res = await chain[i](action, context);
76
- context[`__resolver_${i}`] = res;
77
- lastResult = res;
74
+ const res = await resolver(action, context);
75
+ if (res !== undefined) {
76
+ // Store result in context for debugging
77
+ context[`__resolver_${i}`] = res;
78
+ if (verbose) {
79
+ console.log(`[✅ ${resolver.resolverName || 'anonymous'}] handled action`);
80
+ }
81
+ return res;
82
+ }
78
83
  } catch (e) {
79
- console.error(` ❌ Resolver ${i} failed for action "${action}":`, e.message);
84
+ console.error(` ❌ Resolver ${resolver.resolverName || 'anonymous'} failed:`, e.message);
80
85
  }
81
86
  }
82
- if (verbose) console.log(`[Resolver Chain] action="${action}" lastResult=`, lastResult);
83
- return lastResult;
87
+ if (verbose) {
88
+ console.log(`[⏭️] No resolver handled action: "${action}"`);
89
+ }
90
+ return undefined;
84
91
  };
85
- wrapped._chain = chain;
92
+ wrapped._chain = resolvers;
86
93
  return wrapped;
87
94
  }
88
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
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
@@ -29,25 +29,49 @@ function parse(code, fileName = null) {
29
29
  let line = lines[i];
30
30
 
31
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;
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
+ }
49
+ continue;
40
50
  }
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);
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++;
61
+ continue;
62
+ }
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++;
73
+ continue;
46
74
  }
47
- i++;
48
- }
49
- continue;
50
- }
51
75
 
52
76
  // ---- Math step patterns (standalone) ----
53
77
  let mathAdd = line.match(/^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
@@ -67,7 +91,7 @@ if (allowMatch) {
67
91
  workflow.__requiresMath = true;
68
92
  workflow.steps.push({
69
93
  type: 'calculate',
70
- expression: `subtract({${mathAdd[2]}}, {${mathAdd[1]}})`,
94
+ expression: `subtract({${mathSub[2]}}, {${mathSub[1]}})`, // Fixed: was mathAdd
71
95
  saveAs: mathSub[3].trim()
72
96
  });
73
97
  i++;
@@ -230,7 +254,7 @@ if (allowMatch) {
230
254
  }
231
255
 
232
256
  // ---------------------------
233
- // Parse nested blocks (unchanged)
257
+ // Parse nested blocks (updated)
234
258
  // ---------------------------
235
259
  function parseBlock(lines) {
236
260
  const steps = [];
@@ -270,6 +294,24 @@ function parseBlock(lines) {
270
294
  if (askMatch) {
271
295
  steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
272
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
+ }
273
315
  }
274
316
  return steps;
275
317
  }
package/src/runtime.js CHANGED
@@ -16,6 +16,80 @@ class RuntimeAPI {
16
16
  if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
17
17
  this.disallowedLogFile = path.join(logsDir, 'disallowed_resolvers.json');
18
18
  this.disallowedAttempts = [];
19
+
20
+ // ✅ NEW: Database client setup
21
+ this.dbClient = null;
22
+ this._initDbClient();
23
+ }
24
+
25
+ // ✅ NEW: Initialize database client
26
+ _initDbClient() {
27
+ const dbType = process.env.OLANG_DB_TYPE; // 'postgres', 'mysql', 'mongodb', 'sqlite'
28
+
29
+ if (!dbType) return; // DB persistence disabled
30
+
31
+ try {
32
+ switch (dbType.toLowerCase()) {
33
+ case 'postgres':
34
+ case 'postgresql':
35
+ const { Pool } = require('pg');
36
+ this.dbClient = {
37
+ type: 'postgres',
38
+ client: new Pool({
39
+ host: process.env.DB_HOST || 'localhost',
40
+ port: parseInt(process.env.DB_PORT) || 5432,
41
+ user: process.env.DB_USER,
42
+ password: process.env.DB_PASSWORD,
43
+ database: process.env.DB_NAME
44
+ })
45
+ };
46
+ break;
47
+
48
+ case 'mysql':
49
+ const mysql = require('mysql2/promise');
50
+ this.dbClient = {
51
+ type: 'mysql',
52
+ client: mysql.createPool({
53
+ host: process.env.DB_HOST || 'localhost',
54
+ port: parseInt(process.env.DB_PORT) || 3306,
55
+ user: process.env.DB_USER,
56
+ password: process.env.DB_PASSWORD,
57
+ database: process.env.DB_NAME
58
+ })
59
+ };
60
+ break;
61
+
62
+ case 'mongodb':
63
+ const { MongoClient } = require('mongodb');
64
+ const uri = process.env.MONGO_URI || `mongodb://${process.env.DB_HOST || 'localhost'}:${process.env.DB_PORT || 27017}`;
65
+ this.dbClient = {
66
+ type: 'mongodb',
67
+ client: new MongoClient(uri)
68
+ };
69
+ break;
70
+
71
+ case 'sqlite':
72
+ const Database = require('better-sqlite3');
73
+ const dbPath = process.env.SQLITE_PATH || './olang.db';
74
+ const dbDir = path.dirname(path.resolve(dbPath));
75
+ if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
76
+ this.dbClient = {
77
+ type: 'sqlite',
78
+ client: new Database(dbPath)
79
+ };
80
+ break;
81
+
82
+ default:
83
+ throw new Error(`Unsupported database type: ${dbType}`);
84
+ }
85
+
86
+ if (this.verbose) {
87
+ console.log(`🗄️ Database client initialized: ${dbType}`);
88
+ }
89
+ } catch (e) {
90
+ this.addWarning(`Failed to initialize DB client: ${e.message}`);
91
+ this.dbClient = null;
92
+ }
19
93
  }
20
94
 
21
95
  // -----------------------------
@@ -86,7 +160,7 @@ class RuntimeAPI {
86
160
  const gt = cond.match(/^\{(.+)\}\s+greater than\s+(\d+\.?\d*)$/);
87
161
  if (gt) return parseFloat(this.getNested(ctx, gt[1])) > parseFloat(gt[2]);
88
162
  const lt = cond.match(/^\{(.+)\}\s+less than\s+(\d+\.?\d*)$/);
89
- if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(gt[2]);
163
+ if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(lt[2]);
90
164
  return Boolean(this.getNested(ctx, cond.replace(/\{|\}/g, '')));
91
165
  }
92
166
 
@@ -170,28 +244,43 @@ class RuntimeAPI {
170
244
  this.allowedResolvers.add('builtInMathResolver');
171
245
  }
172
246
 
247
+ // Handle different resolver input formats
248
+ let resolversToRun = [];
249
+
173
250
  if (agentResolver && Array.isArray(agentResolver._chain)) {
174
- for (let idx = 0; idx < agentResolver._chain.length; idx++) {
175
- const resolver = agentResolver._chain[idx];
176
- validateResolver(resolver);
177
-
178
- try {
179
- const out = await resolver(action, this.context);
180
- outputs.push(out);
181
- this.context[`__resolver_${idx}`] = out;
182
- } catch (e) {
183
- this.addWarning(`Resolver ${resolver?.name || idx} failed for action "${action}": ${e.message}`);
184
- outputs.push(null);
251
+ // Resolver chain mode
252
+ resolversToRun = agentResolver._chain;
253
+ } else if (Array.isArray(agentResolver)) {
254
+ // Array of resolvers mode (what npx olang passes with -r flags)
255
+ resolversToRun = agentResolver;
256
+ } else if (agentResolver) {
257
+ // Single resolver mode
258
+ resolversToRun = [agentResolver];
259
+ }
260
+
261
+ // ✅ Return the FIRST resolver that returns a non-undefined result
262
+ for (let idx = 0; idx < resolversToRun.length; idx++) {
263
+ const resolver = resolversToRun[idx];
264
+ validateResolver(resolver);
265
+
266
+ try {
267
+ const out = await resolver(action, this.context);
268
+ outputs.push(out);
269
+ this.context[`__resolver_${idx}`] = out;
270
+
271
+ // ✅ If resolver handled the action (returned non-undefined), use it immediately
272
+ if (out !== undefined) {
273
+ return out;
185
274
  }
275
+ } catch (e) {
276
+ this.addWarning(`Resolver ${resolver?.resolverName || resolver?.name || idx} failed for action "${action}": ${e.message}`);
277
+ outputs.push(null);
278
+ this.context[`__resolver_${idx}`] = null;
186
279
  }
187
- } else {
188
- validateResolver(agentResolver);
189
- const out = await agentResolver(action, this.context);
190
- outputs.push(out);
191
- this.context['__resolver_0'] = out;
192
280
  }
193
281
 
194
- return outputs[outputs.length - 1];
282
+ // If no resolver handled the action, return undefined
283
+ return undefined;
195
284
  };
196
285
 
197
286
  switch (stepType) {
@@ -265,6 +354,95 @@ class RuntimeAPI {
265
354
  this.emit('debrief', { agent: step.agent, message: step.message });
266
355
  break;
267
356
  }
357
+
358
+ // ✅ File Persist step handler
359
+ case 'persist': {
360
+ const sourceValue = this.getNested(this.context, step.source);
361
+ if (sourceValue === undefined) {
362
+ this.addWarning(`Cannot persist undefined value from "${step.source}" to "${step.destination}"`);
363
+ break;
364
+ }
365
+
366
+ const outputPath = path.resolve(process.cwd(), step.destination);
367
+ const outputDir = path.dirname(outputPath);
368
+ if (!fs.existsSync(outputDir)) {
369
+ fs.mkdirSync(outputDir, { recursive: true });
370
+ }
371
+
372
+ let content;
373
+ if (step.destination.endsWith('.json')) {
374
+ content = JSON.stringify(sourceValue, null, 2);
375
+ } else {
376
+ content = String(sourceValue);
377
+ }
378
+
379
+ fs.writeFileSync(outputPath, content, 'utf8');
380
+
381
+ if (this.verbose) {
382
+ console.log(`💾 Persisted "${step.source}" to ${step.destination}`);
383
+ }
384
+ break;
385
+ }
386
+
387
+ // ✅ NEW: Database persist handler
388
+ case 'persist-db': {
389
+ if (!this.dbClient) {
390
+ this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
391
+ break;
392
+ }
393
+
394
+ const sourceValue = this.getNested(this.context, step.source);
395
+ if (sourceValue === undefined) {
396
+ this.addWarning(`Cannot persist undefined value from "${step.source}" to DB collection "${step.collection}"`);
397
+ break;
398
+ }
399
+
400
+ try {
401
+ switch (this.dbClient.type) {
402
+ case 'postgres':
403
+ case 'mysql':
404
+ if (this.dbClient.type === 'postgres') {
405
+ await this.dbClient.client.query(
406
+ `INSERT INTO "${step.collection}" (workflow_name, data, created_at) VALUES ($1, $2, NOW())`,
407
+ [this.context.workflow_name || 'unknown', JSON.stringify(sourceValue)]
408
+ );
409
+ } else {
410
+ await this.dbClient.client.execute(
411
+ `INSERT INTO ?? (workflow_name, data, created_at) VALUES (?, ?, NOW())`,
412
+ [step.collection, this.context.workflow_name || 'unknown', JSON.stringify(sourceValue)]
413
+ );
414
+ }
415
+ break;
416
+
417
+ case 'mongodb':
418
+ const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
419
+ await db.collection(step.collection).insertOne({
420
+ workflow_name: this.context.workflow_name || 'unknown',
421
+ data: sourceValue,
422
+ created_at: new Date()
423
+ });
424
+ break;
425
+
426
+ case 'sqlite':
427
+ const stmt = this.dbClient.client.prepare(
428
+ `INSERT INTO ${step.collection} (workflow_name, data, created_at) VALUES (?, ?, ?)`
429
+ );
430
+ stmt.run(
431
+ this.context.workflow_name || 'unknown',
432
+ JSON.stringify(sourceValue),
433
+ new Date().toISOString()
434
+ );
435
+ break;
436
+ }
437
+
438
+ if (this.verbose) {
439
+ console.log(`🗄️ Persisted "${step.source}" to DB collection ${step.collection}`);
440
+ }
441
+ } catch (e) {
442
+ this.addWarning(`DB persist failed for "${step.source}": ${e.message}`);
443
+ }
444
+ break;
445
+ }
268
446
  }
269
447
 
270
448
  if (this.verbose) {
@@ -274,7 +452,11 @@ class RuntimeAPI {
274
452
  }
275
453
 
276
454
  async executeWorkflow(workflow, inputs, agentResolver) {
277
- this.context = { ...inputs };
455
+ // Inject workflow name into context
456
+ this.context = {
457
+ ...inputs,
458
+ workflow_name: workflow.name
459
+ };
278
460
  this.workflowSteps = workflow.steps;
279
461
  this.allowedResolvers = new Set(workflow.allowedResolvers || []);
280
462
 
@@ -313,4 +495,4 @@ async function execute(workflow, inputs, agentResolver, verbose = false) {
313
495
  return rt.executeWorkflow(workflow, inputs, agentResolver);
314
496
  }
315
497
 
316
- module.exports = { execute, RuntimeAPI };
498
+ module.exports = { execute, RuntimeAPI };