@o-lang/olang 1.0.21 → 1.0.22
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 +18 -11
- package/package.json +1 -1
- package/src/parser.js +61 -19
- package/src/runtime.js +170 -3
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
|
|
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
|
|
73
|
-
|
|
71
|
+
for (let i = 0; i < resolvers.length; i++) {
|
|
72
|
+
const resolver = resolvers[i];
|
|
74
73
|
try {
|
|
75
|
-
const res = await
|
|
76
|
-
|
|
77
|
-
|
|
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 ${
|
|
84
|
+
console.error(` ❌ Resolver ${resolver.resolverName || 'anonymous'} failed:`, e.message);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
|
-
if (verbose)
|
|
83
|
-
|
|
87
|
+
if (verbose) {
|
|
88
|
+
console.log(`[⏭️] No resolver handled action: "${action}"`);
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
84
91
|
};
|
|
85
|
-
wrapped._chain =
|
|
92
|
+
wrapped._chain = resolvers;
|
|
86
93
|
return wrapped;
|
|
87
94
|
}
|
|
88
95
|
|
package/package.json
CHANGED
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
|
-
|
|
33
|
-
if (allowMatch) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
workflow.
|
|
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({${
|
|
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 (
|
|
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(
|
|
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
|
|
|
@@ -265,6 +339,95 @@ class RuntimeAPI {
|
|
|
265
339
|
this.emit('debrief', { agent: step.agent, message: step.message });
|
|
266
340
|
break;
|
|
267
341
|
}
|
|
342
|
+
|
|
343
|
+
// ✅ File Persist step handler
|
|
344
|
+
case 'persist': {
|
|
345
|
+
const sourceValue = this.getNested(this.context, step.source);
|
|
346
|
+
if (sourceValue === undefined) {
|
|
347
|
+
this.addWarning(`Cannot persist undefined value from "${step.source}" to "${step.destination}"`);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const outputPath = path.resolve(process.cwd(), step.destination);
|
|
352
|
+
const outputDir = path.dirname(outputPath);
|
|
353
|
+
if (!fs.existsSync(outputDir)) {
|
|
354
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let content;
|
|
358
|
+
if (step.destination.endsWith('.json')) {
|
|
359
|
+
content = JSON.stringify(sourceValue, null, 2);
|
|
360
|
+
} else {
|
|
361
|
+
content = String(sourceValue);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fs.writeFileSync(outputPath, content, 'utf8');
|
|
365
|
+
|
|
366
|
+
if (this.verbose) {
|
|
367
|
+
console.log(`💾 Persisted "${step.source}" to ${step.destination}`);
|
|
368
|
+
}
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ✅ NEW: Database persist handler
|
|
373
|
+
case 'persist-db': {
|
|
374
|
+
if (!this.dbClient) {
|
|
375
|
+
this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const sourceValue = this.getNested(this.context, step.source);
|
|
380
|
+
if (sourceValue === undefined) {
|
|
381
|
+
this.addWarning(`Cannot persist undefined value from "${step.source}" to DB collection "${step.collection}"`);
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
switch (this.dbClient.type) {
|
|
387
|
+
case 'postgres':
|
|
388
|
+
case 'mysql':
|
|
389
|
+
if (this.dbClient.type === 'postgres') {
|
|
390
|
+
await this.dbClient.client.query(
|
|
391
|
+
`INSERT INTO "${step.collection}" (workflow_name, data, created_at) VALUES ($1, $2, NOW())`,
|
|
392
|
+
[this.context.workflow_name || 'unknown', JSON.stringify(sourceValue)]
|
|
393
|
+
);
|
|
394
|
+
} else {
|
|
395
|
+
await this.dbClient.client.execute(
|
|
396
|
+
`INSERT INTO ?? (workflow_name, data, created_at) VALUES (?, ?, NOW())`,
|
|
397
|
+
[step.collection, this.context.workflow_name || 'unknown', JSON.stringify(sourceValue)]
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
|
|
402
|
+
case 'mongodb':
|
|
403
|
+
const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
|
|
404
|
+
await db.collection(step.collection).insertOne({
|
|
405
|
+
workflow_name: this.context.workflow_name || 'unknown',
|
|
406
|
+
sourceValue,
|
|
407
|
+
created_at: new Date()
|
|
408
|
+
});
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case 'sqlite':
|
|
412
|
+
const stmt = this.dbClient.client.prepare(
|
|
413
|
+
`INSERT INTO ${step.collection} (workflow_name, data, created_at) VALUES (?, ?, ?)`
|
|
414
|
+
);
|
|
415
|
+
stmt.run(
|
|
416
|
+
this.context.workflow_name || 'unknown',
|
|
417
|
+
JSON.stringify(sourceValue),
|
|
418
|
+
new Date().toISOString()
|
|
419
|
+
);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (this.verbose) {
|
|
424
|
+
console.log(`🗄️ Persisted "${step.source}" to DB collection ${step.collection}`);
|
|
425
|
+
}
|
|
426
|
+
} catch (e) {
|
|
427
|
+
this.addWarning(`DB persist failed for "${step.source}": ${e.message}`);
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
268
431
|
}
|
|
269
432
|
|
|
270
433
|
if (this.verbose) {
|
|
@@ -274,7 +437,11 @@ class RuntimeAPI {
|
|
|
274
437
|
}
|
|
275
438
|
|
|
276
439
|
async executeWorkflow(workflow, inputs, agentResolver) {
|
|
277
|
-
|
|
440
|
+
// ✅ Inject workflow name into context
|
|
441
|
+
this.context = {
|
|
442
|
+
...inputs,
|
|
443
|
+
workflow_name: workflow.name
|
|
444
|
+
};
|
|
278
445
|
this.workflowSteps = workflow.steps;
|
|
279
446
|
this.allowedResolvers = new Set(workflow.allowedResolvers || []);
|
|
280
447
|
|
|
@@ -313,4 +480,4 @@ async function execute(workflow, inputs, agentResolver, verbose = false) {
|
|
|
313
480
|
return rt.executeWorkflow(workflow, inputs, agentResolver);
|
|
314
481
|
}
|
|
315
482
|
|
|
316
|
-
module.exports = { execute, RuntimeAPI };
|
|
483
|
+
module.exports = { execute, RuntimeAPI };
|