@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 +18 -11
- package/package.json +1 -1
- package/src/parser.js +61 -19
- package/src/runtime.js +202 -20
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
|
|
|
@@ -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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|