@o-lang/olang 1.0.27 → 1.1.2
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 +7 -1
- package/package.json +1 -1
- package/src/parser.js +432 -94
- package/src/runtime.js +243 -58
package/cli.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
const { Command } = require('commander');
|
|
3
3
|
const { parse } = require('./src/parser');
|
|
4
4
|
const { execute } = require('./src/runtime');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
|
|
8
|
+
// === ADDED: Load package.json for version (1 line added) ===
|
|
9
|
+
const pkg = require('./package.json');
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Enforce .ol extension ONLY (CLI only)
|
|
10
13
|
*/
|
|
@@ -156,6 +159,9 @@ function loadResolverChain(specifiers, verbose, allowed) {
|
|
|
156
159
|
*/
|
|
157
160
|
const program = new Command();
|
|
158
161
|
|
|
162
|
+
// === ADDED: Version support (1 line added) ===
|
|
163
|
+
program.version(pkg.version, '-V, --version', 'Show O-lang kernel version');
|
|
164
|
+
|
|
159
165
|
// === RUN COMMAND ===
|
|
160
166
|
program
|
|
161
167
|
.name('olang')
|
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
|
|
3
|
+
// ✅ Symbol normalization helper (backward compatible)
|
|
4
|
+
function normalizeSymbol(raw) {
|
|
5
|
+
if (!raw) return raw;
|
|
6
|
+
// Take only the first word (stop at first whitespace)
|
|
7
|
+
// Keep letters, numbers, underscores, and $ (for JS compatibility)
|
|
8
|
+
return raw.split(/\s+/)[0].replace(/[^\w$]/g, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
function parse(content, filename = '<unknown>') {
|
|
4
12
|
if (typeof content === 'string') {
|
|
13
|
+
// ✅ Strip UTF-8 BOM if present (0xFEFF = Unicode BOM)
|
|
14
|
+
if (content.charCodeAt(0) === 0xFEFF) {
|
|
15
|
+
content = content.slice(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
const lines = content.split('\n').map(line => line.replace(/\r$/, ''));
|
|
6
19
|
return parseLines(lines, filename);
|
|
7
20
|
} else if (typeof content === 'object' && content !== null) {
|
|
8
|
-
// Already parsed
|
|
9
21
|
return content;
|
|
10
22
|
} else {
|
|
11
23
|
throw new Error('parse() expects string content or pre-parsed object');
|
|
@@ -13,12 +25,15 @@ function parse(content, filename = '<unknown>') {
|
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
function parseFromFile(filepath) {
|
|
28
|
+
// Enforce .ol extension
|
|
29
|
+
if (!filepath.endsWith(".ol")) {
|
|
30
|
+
throw new Error(`Expected .ol workflow, got: ${filepath}`);
|
|
31
|
+
}
|
|
16
32
|
const content = fs.readFileSync(filepath, 'utf8');
|
|
17
33
|
return parse(content, filepath);
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
function parseLines(lines, filename) {
|
|
21
|
-
// Remove evolution file parsing - evolution is now in-workflow
|
|
22
37
|
return parseWorkflowLines(lines, filename);
|
|
23
38
|
}
|
|
24
39
|
|
|
@@ -30,27 +45,40 @@ function parseWorkflowLines(lines, filename) {
|
|
|
30
45
|
steps: [],
|
|
31
46
|
returnValues: [],
|
|
32
47
|
allowedResolvers: [],
|
|
33
|
-
maxGenerations: null,
|
|
48
|
+
maxGenerations: null,
|
|
34
49
|
__warnings: [],
|
|
35
50
|
filename: filename
|
|
36
51
|
};
|
|
37
|
-
|
|
52
|
+
|
|
38
53
|
let i = 0;
|
|
39
54
|
let currentStep = null;
|
|
40
55
|
let inAllowResolvers = false;
|
|
41
56
|
let inIfBlock = false;
|
|
42
57
|
let ifCondition = null;
|
|
43
58
|
let ifBody = [];
|
|
44
|
-
|
|
59
|
+
let inParallelBlock = false;
|
|
60
|
+
let parallelSteps = [];
|
|
61
|
+
let parallelTimeout = null;
|
|
62
|
+
let inEscalationBlock = false;
|
|
63
|
+
let escalationLevels = [];
|
|
64
|
+
let currentLevel = null;
|
|
65
|
+
|
|
66
|
+
// ✅ Helper to flush currentStep
|
|
67
|
+
const flushCurrentStep = () => {
|
|
68
|
+
if (currentStep) {
|
|
69
|
+
workflow.steps.push(currentStep);
|
|
70
|
+
currentStep = null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
45
74
|
while (i < lines.length) {
|
|
46
75
|
let line = lines[i++].trim();
|
|
47
|
-
|
|
48
|
-
// Skip empty lines and comments
|
|
76
|
+
|
|
49
77
|
if (line === '' || line.startsWith('#')) {
|
|
50
78
|
continue;
|
|
51
79
|
}
|
|
52
|
-
|
|
53
|
-
//
|
|
80
|
+
|
|
81
|
+
// Workflow declaration
|
|
54
82
|
if (line.startsWith('Workflow ')) {
|
|
55
83
|
const match = line.match(/^Workflow\s+"([^"]+)"(?:\s+with\s+(.+))?$/i);
|
|
56
84
|
if (match) {
|
|
@@ -63,8 +91,8 @@ function parseWorkflowLines(lines, filename) {
|
|
|
63
91
|
}
|
|
64
92
|
continue;
|
|
65
93
|
}
|
|
66
|
-
|
|
67
|
-
//
|
|
94
|
+
|
|
95
|
+
// Global Constraint: max_generations
|
|
68
96
|
if (line.startsWith('Constraint: max_generations = ')) {
|
|
69
97
|
const match = line.match(/^Constraint:\s+max_generations\s*=\s*(\d+)$/i);
|
|
70
98
|
if (match) {
|
|
@@ -74,13 +102,13 @@ function parseWorkflowLines(lines, filename) {
|
|
|
74
102
|
}
|
|
75
103
|
continue;
|
|
76
104
|
}
|
|
77
|
-
|
|
78
|
-
//
|
|
105
|
+
|
|
106
|
+
// Allow resolvers section
|
|
79
107
|
if (line === 'Allow resolvers:') {
|
|
80
108
|
inAllowResolvers = true;
|
|
81
109
|
continue;
|
|
82
110
|
}
|
|
83
|
-
|
|
111
|
+
|
|
84
112
|
if (inAllowResolvers) {
|
|
85
113
|
if (line.startsWith('- ')) {
|
|
86
114
|
const resolverName = line.substring(2).trim();
|
|
@@ -88,28 +116,153 @@ function parseWorkflowLines(lines, filename) {
|
|
|
88
116
|
workflow.allowedResolvers.push(resolverName);
|
|
89
117
|
}
|
|
90
118
|
} else if (line === '' || line.startsWith('#')) {
|
|
91
|
-
|
|
119
|
+
continue;
|
|
92
120
|
} else {
|
|
93
|
-
// End of Allow resolvers section
|
|
94
121
|
inAllowResolvers = false;
|
|
95
|
-
i--;
|
|
122
|
+
i--;
|
|
96
123
|
continue;
|
|
97
124
|
}
|
|
98
125
|
continue;
|
|
99
126
|
}
|
|
100
|
-
|
|
101
|
-
// Parse
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
|
|
128
|
+
// ✅ Parse Escalation block
|
|
129
|
+
if (line.match(/^Run in parallel with escalation:$/i)) {
|
|
130
|
+
flushCurrentStep();
|
|
131
|
+
inEscalationBlock = true;
|
|
132
|
+
escalationLevels = [];
|
|
133
|
+
currentLevel = null;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (inEscalationBlock) {
|
|
138
|
+
if (line.match(/^End$/i)) {
|
|
139
|
+
if (currentLevel) {
|
|
140
|
+
escalationLevels.push(currentLevel);
|
|
141
|
+
}
|
|
142
|
+
workflow.steps.push({
|
|
143
|
+
type: 'escalation',
|
|
144
|
+
levels: escalationLevels,
|
|
145
|
+
stepNumber: workflow.steps.length + 1
|
|
146
|
+
});
|
|
147
|
+
inEscalationBlock = false;
|
|
148
|
+
continue;
|
|
149
|
+
} else if (line.match(/^Level \d+:/i)) {
|
|
150
|
+
// Parse level declaration
|
|
151
|
+
const levelMatch = line.match(/^Level (\d+):\s+(.+)$/i);
|
|
152
|
+
if (levelMatch) {
|
|
153
|
+
if (currentLevel) {
|
|
154
|
+
escalationLevels.push(currentLevel);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Parse timeout from level description
|
|
158
|
+
let timeoutMs = null;
|
|
159
|
+
const desc = levelMatch[2].trim().toLowerCase();
|
|
160
|
+
if (desc.includes('immediately')) {
|
|
161
|
+
timeoutMs = 0;
|
|
162
|
+
} else {
|
|
163
|
+
const timeMatch = desc.match(/within\s+(\d+)\s*([smhd])/i);
|
|
164
|
+
if (timeMatch) {
|
|
165
|
+
const value = parseInt(timeMatch[1]);
|
|
166
|
+
const unit = timeMatch[2].toLowerCase();
|
|
167
|
+
const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
168
|
+
timeoutMs = value * (multipliers[unit] || 1000);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
currentLevel = {
|
|
173
|
+
levelNumber: parseInt(levelMatch[1]),
|
|
174
|
+
timeout: timeoutMs,
|
|
175
|
+
steps: []
|
|
176
|
+
};
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
} else if (currentLevel) {
|
|
180
|
+
// Parse steps within level
|
|
181
|
+
currentLevel.steps.push(line);
|
|
182
|
+
continue;
|
|
108
183
|
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ✅ Parse Timed Parallel block (EXACT FORMAT - NO DUPLICATION)
|
|
187
|
+
const timedParMatch = line.match(/^Run in parallel for (\d+)\s*([smhd])$/i);
|
|
188
|
+
if (timedParMatch) {
|
|
189
|
+
flushCurrentStep();
|
|
190
|
+
|
|
191
|
+
const value = parseInt(timedParMatch[1]);
|
|
192
|
+
const unit = timedParMatch[2].toLowerCase();
|
|
193
|
+
const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
194
|
+
const timeoutMs = value * (multipliers[unit] || 1000);
|
|
109
195
|
|
|
196
|
+
inParallelBlock = true;
|
|
197
|
+
parallelSteps = [];
|
|
198
|
+
parallelTimeout = timeoutMs;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ✅ Parse Normal Parallel block (backward compatible)
|
|
203
|
+
if (line.match(/^Run in parallel$/i)) {
|
|
204
|
+
flushCurrentStep();
|
|
205
|
+
inParallelBlock = true;
|
|
206
|
+
parallelSteps = [];
|
|
207
|
+
parallelTimeout = null;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (inParallelBlock) {
|
|
212
|
+
if (line.match(/^End$/i)) {
|
|
213
|
+
flushCurrentStep(); // ✅ Flush last parallel step
|
|
214
|
+
const parsedParallel = parseBlock(parallelSteps);
|
|
215
|
+
workflow.steps.push({
|
|
216
|
+
type: 'parallel',
|
|
217
|
+
steps: parsedParallel,
|
|
218
|
+
timeout: parallelTimeout,
|
|
219
|
+
stepNumber: workflow.steps.length + 1
|
|
220
|
+
});
|
|
221
|
+
inParallelBlock = false;
|
|
222
|
+
parallelTimeout = null;
|
|
223
|
+
continue;
|
|
224
|
+
} else {
|
|
225
|
+
parallelSteps.push(line);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ✅ FLUSH before If/When block
|
|
231
|
+
if (line.match(/^(?:If|When)\s+(.+)$/i)) {
|
|
232
|
+
flushCurrentStep();
|
|
233
|
+
const ifMatch = line.match(/^(?:If|When)\s+(.+)$/i);
|
|
234
|
+
ifCondition = ifMatch[1].trim();
|
|
235
|
+
inIfBlock = true;
|
|
236
|
+
ifBody = [];
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (inIfBlock) {
|
|
241
|
+
if (line.match(/^End(?:If)?$/i)) {
|
|
242
|
+
flushCurrentStep(); // ✅ Flush last if step
|
|
243
|
+
const parsedIfBody = parseBlock(ifBody);
|
|
244
|
+
workflow.steps.push({
|
|
245
|
+
type: 'if',
|
|
246
|
+
condition: ifCondition,
|
|
247
|
+
body: parsedIfBody,
|
|
248
|
+
stepNumber: workflow.steps.length + 1
|
|
249
|
+
});
|
|
250
|
+
inIfBlock = false;
|
|
251
|
+
ifCondition = null;
|
|
252
|
+
ifBody = [];
|
|
253
|
+
continue;
|
|
254
|
+
} else {
|
|
255
|
+
ifBody.push(line);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Step declaration
|
|
261
|
+
const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
|
|
262
|
+
if (stepMatch) {
|
|
263
|
+
flushCurrentStep(); // ✅ Flush previous step
|
|
110
264
|
const stepNumber = parseInt(stepMatch[1], 10);
|
|
111
265
|
const stepContent = stepMatch[2];
|
|
112
|
-
|
|
113
266
|
currentStep = {
|
|
114
267
|
type: 'action',
|
|
115
268
|
stepNumber: stepNumber,
|
|
@@ -119,81 +272,138 @@ function parseWorkflowLines(lines, filename) {
|
|
|
119
272
|
};
|
|
120
273
|
continue;
|
|
121
274
|
}
|
|
122
|
-
|
|
123
|
-
//
|
|
275
|
+
|
|
276
|
+
// Save as - ✅ Apply normalization
|
|
277
|
+
const saveMatch = line.match(/^Save as\s+(.+)$/i);
|
|
278
|
+
if (saveMatch && currentStep) {
|
|
279
|
+
currentStep.saveAs = normalizeSymbol(saveMatch[1].trim());
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Constraint (per-step)
|
|
284
|
+
const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
|
|
285
|
+
if (constraintMatch && currentStep) {
|
|
286
|
+
const constraintLine = constraintMatch[1].trim();
|
|
287
|
+
const eq = constraintLine.match(/^([^=]+)=\s*(.+)$/);
|
|
288
|
+
if (eq) {
|
|
289
|
+
let key = eq[1].trim();
|
|
290
|
+
let value = eq[2].trim();
|
|
291
|
+
|
|
292
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
293
|
+
value = value.slice(1, -1).split(',').map(v => v.trim().replace(/^"/, '').replace(/"$/, ''));
|
|
294
|
+
} else if (!isNaN(value)) {
|
|
295
|
+
value = Number(value);
|
|
296
|
+
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
297
|
+
value = value.slice(1, -1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
currentStep.constraints[key] = value;
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Debrief
|
|
306
|
+
const debriefMatch = line.match(/^Debrief\s+([^\s]+)\s+with\s+"([^"]*)"$/i);
|
|
307
|
+
if (debriefMatch) {
|
|
308
|
+
flushCurrentStep();
|
|
309
|
+
workflow.steps.push({
|
|
310
|
+
type: 'debrief',
|
|
311
|
+
agent: debriefMatch[1].trim(),
|
|
312
|
+
message: debriefMatch[2],
|
|
313
|
+
stepNumber: workflow.steps.length + 1
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Evolve
|
|
124
319
|
const evolveMatch = line.match(/^Evolve\s+([^\s]+)\s+using\s+feedback:\s*"([^"]*)"$/i);
|
|
125
320
|
if (evolveMatch) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
currentStep = null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
currentStep = {
|
|
321
|
+
flushCurrentStep();
|
|
322
|
+
workflow.steps.push({
|
|
132
323
|
type: 'evolve',
|
|
133
|
-
stepNumber: workflow.steps.length + 1,
|
|
134
324
|
targetResolver: evolveMatch[1].trim(),
|
|
135
325
|
feedback: evolveMatch[2],
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
};
|
|
326
|
+
stepNumber: workflow.steps.length + 1
|
|
327
|
+
});
|
|
139
328
|
continue;
|
|
140
329
|
}
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
145
|
-
|
|
330
|
+
|
|
331
|
+
// Prompt
|
|
332
|
+
const promptMatch = line.match(/^Prompt user to\s+"([^"]*)"$/i);
|
|
333
|
+
if (promptMatch) {
|
|
334
|
+
flushCurrentStep();
|
|
335
|
+
workflow.steps.push({
|
|
336
|
+
type: 'prompt',
|
|
337
|
+
question: promptMatch[1],
|
|
338
|
+
stepNumber: workflow.steps.length + 1,
|
|
339
|
+
saveAs: null
|
|
340
|
+
});
|
|
146
341
|
continue;
|
|
147
342
|
}
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
343
|
+
|
|
344
|
+
// Persist
|
|
345
|
+
const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]*)"$/i);
|
|
346
|
+
if (persistMatch) {
|
|
347
|
+
flushCurrentStep();
|
|
348
|
+
workflow.steps.push({
|
|
349
|
+
type: 'persist',
|
|
350
|
+
variable: persistMatch[1].trim(),
|
|
351
|
+
target: persistMatch[2],
|
|
352
|
+
stepNumber: workflow.steps.length + 1
|
|
353
|
+
});
|
|
155
354
|
continue;
|
|
156
355
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
currentStep = null;
|
|
163
|
-
}
|
|
164
|
-
|
|
356
|
+
|
|
357
|
+
// Emit
|
|
358
|
+
const emitMatch = line.match(/^Emit\s+"([^"]+)"\s+with\s+(.+)$/i);
|
|
359
|
+
if (emitMatch) {
|
|
360
|
+
flushCurrentStep();
|
|
165
361
|
workflow.steps.push({
|
|
166
|
-
type: '
|
|
167
|
-
|
|
168
|
-
|
|
362
|
+
type: 'emit',
|
|
363
|
+
event: emitMatch[1],
|
|
364
|
+
payload: emitMatch[2].trim(),
|
|
169
365
|
stepNumber: workflow.steps.length + 1
|
|
170
366
|
});
|
|
171
|
-
|
|
172
|
-
inIfBlock = false;
|
|
173
|
-
ifCondition = null;
|
|
174
|
-
ifBody = [];
|
|
175
367
|
continue;
|
|
176
368
|
}
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
369
|
+
|
|
370
|
+
// Use (for Notify-like actions)
|
|
371
|
+
const useMatch = line.match(/^Use\s+(.+)$/i);
|
|
372
|
+
if (useMatch) {
|
|
373
|
+
flushCurrentStep();
|
|
374
|
+
workflow.steps.push({
|
|
375
|
+
type: 'use',
|
|
376
|
+
tool: useMatch[1].trim(),
|
|
377
|
+
stepNumber: workflow.steps.length + 1,
|
|
378
|
+
saveAs: null,
|
|
379
|
+
constraints: {}
|
|
380
|
+
});
|
|
181
381
|
continue;
|
|
182
382
|
}
|
|
183
|
-
|
|
184
|
-
//
|
|
383
|
+
|
|
384
|
+
// Ask (for Notify/resolver calls)
|
|
385
|
+
const askMatch = line.match(/^Ask\s+(.+)$/i);
|
|
386
|
+
if (askMatch) {
|
|
387
|
+
flushCurrentStep();
|
|
388
|
+
workflow.steps.push({
|
|
389
|
+
type: 'ask',
|
|
390
|
+
target: askMatch[1].trim(),
|
|
391
|
+
stepNumber: workflow.steps.length + 1,
|
|
392
|
+
saveAs: null,
|
|
393
|
+
constraints: {}
|
|
394
|
+
});
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Return
|
|
185
399
|
const returnMatch = line.match(/^Return\s+(.+)$/i);
|
|
186
400
|
if (returnMatch) {
|
|
187
|
-
|
|
188
|
-
workflow.steps.push(currentStep);
|
|
189
|
-
currentStep = null;
|
|
190
|
-
}
|
|
401
|
+
flushCurrentStep();
|
|
191
402
|
workflow.returnValues = returnMatch[1].split(',').map(r => r.trim()).filter(r => r !== '');
|
|
192
403
|
continue;
|
|
193
404
|
}
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
// Try to handle it as a step
|
|
405
|
+
|
|
406
|
+
// Fallback: treat as action
|
|
197
407
|
if (line.trim() !== '') {
|
|
198
408
|
if (!currentStep) {
|
|
199
409
|
currentStep = {
|
|
@@ -204,51 +414,179 @@ function parseWorkflowLines(lines, filename) {
|
|
|
204
414
|
constraints: {}
|
|
205
415
|
};
|
|
206
416
|
} else {
|
|
207
|
-
// Append to current step action (multi-line)
|
|
208
417
|
currentStep.actionRaw += ' ' + line;
|
|
209
418
|
}
|
|
210
419
|
}
|
|
211
420
|
}
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Post-process steps to extract Save as from actionRaw
|
|
421
|
+
|
|
422
|
+
flushCurrentStep(); // ✅ Final flush
|
|
423
|
+
|
|
424
|
+
// Post-process Save as in actionRaw - ✅ Apply normalization
|
|
219
425
|
workflow.steps.forEach(step => {
|
|
220
426
|
if (step.actionRaw && step.saveAs === null) {
|
|
221
427
|
const saveInAction = step.actionRaw.match(/(.+?)\s+Save as\s+(.+)$/i);
|
|
222
428
|
if (saveInAction) {
|
|
223
429
|
step.actionRaw = saveInAction[1].trim();
|
|
224
|
-
step.saveAs = saveInAction[2].trim();
|
|
430
|
+
step.saveAs = normalizeSymbol(saveInAction[2].trim());
|
|
225
431
|
}
|
|
226
432
|
}
|
|
433
|
+
// ✅ Also normalize any existing saveAs values
|
|
434
|
+
if (step.saveAs) {
|
|
435
|
+
step.saveAs = normalizeSymbol(step.saveAs);
|
|
436
|
+
}
|
|
227
437
|
});
|
|
228
|
-
|
|
229
|
-
//
|
|
438
|
+
|
|
439
|
+
// Validation warnings
|
|
230
440
|
if (!workflow.name) {
|
|
231
441
|
workflow.__warnings.push('Workflow name not found');
|
|
232
442
|
}
|
|
233
|
-
|
|
234
443
|
if (workflow.steps.length === 0) {
|
|
235
444
|
workflow.__warnings.push('No steps found in workflow');
|
|
236
445
|
}
|
|
237
|
-
|
|
238
446
|
if (workflow.returnValues.length === 0 && workflow.steps.length > 0) {
|
|
239
447
|
workflow.__warnings.push('No Return statement found');
|
|
240
448
|
}
|
|
241
|
-
|
|
449
|
+
|
|
242
450
|
return workflow;
|
|
243
451
|
}
|
|
244
452
|
|
|
453
|
+
// Parses blocks (for parallel, if, escalation levels)
|
|
454
|
+
function parseBlock(lines) {
|
|
455
|
+
const steps = [];
|
|
456
|
+
let current = null;
|
|
457
|
+
|
|
458
|
+
const flush = () => {
|
|
459
|
+
if (current) {
|
|
460
|
+
steps.push(current);
|
|
461
|
+
current = null;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
for (let line of lines) {
|
|
466
|
+
line = line.trim();
|
|
467
|
+
if (!line || line.startsWith('#')) continue;
|
|
468
|
+
|
|
469
|
+
const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
|
|
470
|
+
if (stepMatch) {
|
|
471
|
+
flush();
|
|
472
|
+
current = {
|
|
473
|
+
type: 'action',
|
|
474
|
+
stepNumber: parseInt(stepMatch[1], 10),
|
|
475
|
+
actionRaw: stepMatch[2].trim(),
|
|
476
|
+
saveAs: null,
|
|
477
|
+
constraints: {}
|
|
478
|
+
};
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Save as - ✅ Apply normalization
|
|
483
|
+
const saveMatch = line.match(/^Save as\s+(.+)$/i);
|
|
484
|
+
if (saveMatch && current) {
|
|
485
|
+
current.saveAs = normalizeSymbol(saveMatch[1].trim());
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Handle all special steps inside blocks
|
|
490
|
+
const debriefMatch = line.match(/^Debrief\s+([^\s]+)\s+with\s+"([^"]*)"$/i);
|
|
491
|
+
if (debriefMatch) {
|
|
492
|
+
flush();
|
|
493
|
+
steps.push({ type: 'debrief', agent: debriefMatch[1].trim(), message: debriefMatch[2] });
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const evolveMatch = line.match(/^Evolve\s+([^\s]+)\s+using\s+feedback:\s*"([^"]*)"$/i);
|
|
498
|
+
if (evolveMatch) {
|
|
499
|
+
flush();
|
|
500
|
+
steps.push({ type: 'evolve', targetResolver: evolveMatch[1].trim(), feedback: evolveMatch[2] });
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const promptMatch = line.match(/^Prompt user to\s+"([^"]*)"$/i);
|
|
505
|
+
if (promptMatch) {
|
|
506
|
+
flush();
|
|
507
|
+
steps.push({ type: 'prompt', question: promptMatch[1], saveAs: null });
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]*)"$/i);
|
|
512
|
+
if (persistMatch) {
|
|
513
|
+
flush();
|
|
514
|
+
steps.push({ type: 'persist', variable: persistMatch[1].trim(), target: persistMatch[2] });
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const emitMatch = line.match(/^Emit\s+"([^"]+)"\s+with\s+(.+)$/i);
|
|
519
|
+
if (emitMatch) {
|
|
520
|
+
flush();
|
|
521
|
+
steps.push({ type: 'emit', event: emitMatch[1], payload: emitMatch[2].trim() });
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const useMatch = line.match(/^Use\s+(.+)$/i);
|
|
526
|
+
if (useMatch) {
|
|
527
|
+
flush();
|
|
528
|
+
steps.push({ type: 'use', tool: useMatch[1].trim(), saveAs: null, constraints: {} });
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const askMatch = line.match(/^Ask\s+(.+)$/i);
|
|
533
|
+
if (askMatch) {
|
|
534
|
+
flush();
|
|
535
|
+
steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Constraint inside block
|
|
540
|
+
const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
|
|
541
|
+
if (constraintMatch && current) {
|
|
542
|
+
const constraintLine = constraintMatch[1].trim();
|
|
543
|
+
const eq = constraintLine.match(/^([^=]+)=\s*(.+)$/);
|
|
544
|
+
if (eq) {
|
|
545
|
+
let key = eq[1].trim();
|
|
546
|
+
let value = eq[2].trim();
|
|
547
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
548
|
+
value = value.slice(1, -1).split(',').map(v => v.trim().replace(/^"/, '').replace(/"$/, ''));
|
|
549
|
+
} else if (!isNaN(value)) {
|
|
550
|
+
value = Number(value);
|
|
551
|
+
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
552
|
+
value = value.slice(1, -1);
|
|
553
|
+
}
|
|
554
|
+
current.constraints[key] = value;
|
|
555
|
+
}
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Fallback
|
|
560
|
+
if (current) {
|
|
561
|
+
current.actionRaw += ' ' + line;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
flush(); // ✅ Final flush in block
|
|
566
|
+
|
|
567
|
+
// ✅ Post-process Save as for steps inside blocks - Apply normalization
|
|
568
|
+
steps.forEach(step => {
|
|
569
|
+
if (step.actionRaw && step.saveAs === null) {
|
|
570
|
+
const saveInAction = step.actionRaw.match(/(.+?)\s+Save as\s+(.+)$/i);
|
|
571
|
+
if (saveInAction) {
|
|
572
|
+
step.actionRaw = saveInAction[1].trim();
|
|
573
|
+
step.saveAs = normalizeSymbol(saveInAction[2].trim());
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// ✅ Also normalize any existing saveAs values
|
|
577
|
+
if (step.saveAs) {
|
|
578
|
+
step.saveAs = normalizeSymbol(step.saveAs);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
return steps;
|
|
583
|
+
}
|
|
584
|
+
|
|
245
585
|
function validate(workflow) {
|
|
246
586
|
const errors = [];
|
|
247
|
-
|
|
248
587
|
if (workflow.maxGenerations !== null && workflow.maxGenerations <= 0) {
|
|
249
588
|
errors.push('max_generations must be positive');
|
|
250
589
|
}
|
|
251
|
-
|
|
252
590
|
return errors;
|
|
253
591
|
}
|
|
254
592
|
|
package/src/runtime.js
CHANGED
|
@@ -92,6 +92,33 @@ class RuntimeAPI {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// -----------------------------
|
|
96
|
+
// ✅ SEMANTIC ENFORCEMENT HELPER
|
|
97
|
+
// -----------------------------
|
|
98
|
+
_requireSemantic(symbol, stepType) {
|
|
99
|
+
const value = this.context[symbol];
|
|
100
|
+
if (value === undefined) {
|
|
101
|
+
const error = {
|
|
102
|
+
type: 'semantic_violation',
|
|
103
|
+
symbol: symbol,
|
|
104
|
+
expected: 'defined value',
|
|
105
|
+
used_by: stepType,
|
|
106
|
+
phase: 'execution'
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Emit semantic event (for observability)
|
|
110
|
+
this.emit('semantic_violation', error);
|
|
111
|
+
|
|
112
|
+
// Log as error (not warning)
|
|
113
|
+
if (this.verbose) {
|
|
114
|
+
console.error(`[O-Lang SEMANTIC] Missing required symbol "${symbol}" for ${stepType}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
95
122
|
// -----------------------------
|
|
96
123
|
// Parser/runtime warnings
|
|
97
124
|
// -----------------------------
|
|
@@ -148,11 +175,6 @@ class RuntimeAPI {
|
|
|
148
175
|
// -----------------------------
|
|
149
176
|
// ✅ ADDITION 1 — External Resolver Detection
|
|
150
177
|
// -----------------------------
|
|
151
|
-
/**
|
|
152
|
-
* Determines whether a resolver is external (HTTP-based)
|
|
153
|
-
* External resolvers MUST be declared via a manifest (.json)
|
|
154
|
-
* and explicitly allowed by workflow policy
|
|
155
|
-
*/
|
|
156
178
|
_isExternalResolver(resolver) {
|
|
157
179
|
return Boolean(
|
|
158
180
|
resolver &&
|
|
@@ -166,13 +188,6 @@ class RuntimeAPI {
|
|
|
166
188
|
// -----------------------------
|
|
167
189
|
// ✅ ADDITION 2 — External Resolver Invocation (HTTP Enforcement)
|
|
168
190
|
// -----------------------------
|
|
169
|
-
/**
|
|
170
|
-
* Calls an external HTTP resolver using its manifest definition.
|
|
171
|
-
* Enforces:
|
|
172
|
-
* - timeout
|
|
173
|
-
* - JSON contract
|
|
174
|
-
* - isolation (no direct execution)
|
|
175
|
-
*/
|
|
176
191
|
async _callExternalResolver(resolver, action, context) {
|
|
177
192
|
const manifest = resolver.manifest;
|
|
178
193
|
const endpoint = manifest.endpoint;
|
|
@@ -291,6 +306,18 @@ class RuntimeAPI {
|
|
|
291
306
|
async executeStep(step, agentResolver) {
|
|
292
307
|
const stepType = step.type;
|
|
293
308
|
|
|
309
|
+
// ✅ Enforce per-step constraints (basic validation)
|
|
310
|
+
if (step.constraints && Object.keys(step.constraints).length > 0) {
|
|
311
|
+
for (const [key, value] of Object.entries(step.constraints)) {
|
|
312
|
+
// Log unsupported constraints (future extensibility)
|
|
313
|
+
if (['max_time_sec', 'cost_limit', 'allowed_resolvers'].includes(key)) {
|
|
314
|
+
this.addWarning(`Per-step constraint "${key}=${value}" is parsed but not yet enforced`);
|
|
315
|
+
} else {
|
|
316
|
+
this.addWarning(`Unknown per-step constraint: ${key}=${value}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
294
321
|
// ✅ ADDITION 3 — Resolver Policy Enforcement (External + Local)
|
|
295
322
|
const enforceResolverPolicy = (resolver, step) => {
|
|
296
323
|
const resolverName = resolver?.resolverName || resolver?.name;
|
|
@@ -316,9 +343,8 @@ class RuntimeAPI {
|
|
|
316
343
|
}
|
|
317
344
|
};
|
|
318
345
|
|
|
346
|
+
// ✅ STRICT SAFETY: Updated runResolvers with failure halting
|
|
319
347
|
const runResolvers = async (action) => {
|
|
320
|
-
const outputs = [];
|
|
321
|
-
|
|
322
348
|
const mathPattern =
|
|
323
349
|
/^(Add|Subtract|Multiply|Divide|Sum|Avg|Min|Max|Round|Floor|Ceil|Abs)\b/i;
|
|
324
350
|
|
|
@@ -344,13 +370,13 @@ class RuntimeAPI {
|
|
|
344
370
|
resolversToRun = [agentResolver];
|
|
345
371
|
}
|
|
346
372
|
|
|
347
|
-
// ✅
|
|
373
|
+
// ✅ STRICT SAFETY: Fail fast on resolver errors or empty results
|
|
348
374
|
for (let idx = 0; idx < resolversToRun.length; idx++) {
|
|
349
375
|
const resolver = resolversToRun[idx];
|
|
350
|
-
enforceResolverPolicy(resolver, step);
|
|
376
|
+
enforceResolverPolicy(resolver, step);
|
|
351
377
|
|
|
352
378
|
try {
|
|
353
|
-
let result;
|
|
379
|
+
let result;
|
|
354
380
|
|
|
355
381
|
if (this._isExternalResolver(resolver)) {
|
|
356
382
|
result = await this._callExternalResolver(
|
|
@@ -362,22 +388,32 @@ class RuntimeAPI {
|
|
|
362
388
|
result = await resolver(action, this.context);
|
|
363
389
|
}
|
|
364
390
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
391
|
+
// ✅ SAFETY GUARD 1: Reject undefined/null results
|
|
392
|
+
if (result === undefined || result === null) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`[O-Lang SAFETY] Resolver "${resolver.resolverName || resolver.name || 'anonymous'}" returned empty result for action: "${action}". ` +
|
|
395
|
+
`Workflow halted to prevent unsafe data propagation.`
|
|
396
|
+
);
|
|
368
397
|
}
|
|
369
398
|
|
|
370
|
-
outputs.push(result);
|
|
371
399
|
this.context[`__resolver_${idx}`] = result;
|
|
400
|
+
return result;
|
|
401
|
+
|
|
372
402
|
} catch (e) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
403
|
+
// ✅ SAFETY GUARD 2: HALT workflow immediately on resolver failure
|
|
404
|
+
throw new Error(
|
|
405
|
+
`[O-Lang SAFETY] Resolver "${resolver?.resolverName || resolver?.name || idx}" failed for action "${action}": ${e.message}\n` +
|
|
406
|
+
`Workflow execution halted.`
|
|
407
|
+
);
|
|
376
408
|
}
|
|
377
409
|
}
|
|
378
410
|
|
|
379
|
-
//
|
|
380
|
-
|
|
411
|
+
// ✅ SAFETY GUARD 3: No resolver handled the action — HALT
|
|
412
|
+
throw new Error(
|
|
413
|
+
`[O-Lang SAFETY] No resolver handled action: "${action}". ` +
|
|
414
|
+
`Available resolvers: ${resolversToRun.map(r => r.resolverName || r.name || 'anonymous').join(', ')}. ` +
|
|
415
|
+
`Workflow execution halted.`
|
|
416
|
+
);
|
|
381
417
|
};
|
|
382
418
|
|
|
383
419
|
switch (stepType) {
|
|
@@ -426,14 +462,12 @@ class RuntimeAPI {
|
|
|
426
462
|
}
|
|
427
463
|
|
|
428
464
|
case 'evolve': {
|
|
429
|
-
// ✅ Handle in-workflow Evolve steps
|
|
430
465
|
const { targetResolver, feedback } = step;
|
|
431
466
|
|
|
432
467
|
if (this.verbose) {
|
|
433
468
|
console.log(`🔄 Evolve step: ${targetResolver} with feedback: "${feedback}"`);
|
|
434
469
|
}
|
|
435
470
|
|
|
436
|
-
// Basic evolution: record the request (free tier)
|
|
437
471
|
const evolutionResult = {
|
|
438
472
|
resolver: targetResolver,
|
|
439
473
|
feedback: feedback,
|
|
@@ -442,12 +476,10 @@ class RuntimeAPI {
|
|
|
442
476
|
workflow: this.context.workflow_name
|
|
443
477
|
};
|
|
444
478
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
evolutionResult.message = 'Advanced evolution service would process this request';
|
|
450
|
-
}
|
|
479
|
+
if (process.env.OLANG_EVOLUTION_API_KEY) {
|
|
480
|
+
evolutionResult.status = 'advanced_evolution_enabled';
|
|
481
|
+
evolutionResult.message = 'Advanced evolution service would process this request';
|
|
482
|
+
}
|
|
451
483
|
|
|
452
484
|
if (step.saveAs) {
|
|
453
485
|
this.context[step.saveAs] = evolutionResult;
|
|
@@ -463,7 +495,99 @@ class RuntimeAPI {
|
|
|
463
495
|
}
|
|
464
496
|
|
|
465
497
|
case 'parallel': {
|
|
466
|
-
|
|
498
|
+
const { steps, timeout } = step;
|
|
499
|
+
|
|
500
|
+
if (timeout !== undefined && timeout > 0) {
|
|
501
|
+
// Timed parallel execution
|
|
502
|
+
const timeoutPromise = new Promise(resolve => {
|
|
503
|
+
setTimeout(() => resolve({ timedOut: true }), timeout);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const parallelPromise = Promise.all(
|
|
507
|
+
steps.map(s => this.executeStep(s, agentResolver))
|
|
508
|
+
).then(() => ({ timedOut: false }));
|
|
509
|
+
|
|
510
|
+
const result = await Promise.race([timeoutPromise, parallelPromise]);
|
|
511
|
+
this.context.timed_out = result.timedOut;
|
|
512
|
+
|
|
513
|
+
if (result.timedOut) {
|
|
514
|
+
this.emit('parallel_timeout', { duration: timeout, steps: steps.length });
|
|
515
|
+
if (this.verbose) {
|
|
516
|
+
console.log(`⏰ Parallel execution timed out after ${timeout}ms`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
// Normal parallel execution (no timeout)
|
|
521
|
+
await Promise.all(steps.map(s => this.executeStep(s, agentResolver)));
|
|
522
|
+
this.context.timed_out = false;
|
|
523
|
+
}
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
case 'escalation': {
|
|
528
|
+
const { levels } = step;
|
|
529
|
+
let finalResult = null;
|
|
530
|
+
let currentTimeout = 0;
|
|
531
|
+
let completedLevel = null;
|
|
532
|
+
|
|
533
|
+
for (const level of levels) {
|
|
534
|
+
if (level.timeout === 0) {
|
|
535
|
+
// Immediate execution (no timeout)
|
|
536
|
+
const levelSteps = require('./parser').parseBlock(level.steps);
|
|
537
|
+
for (const levelStep of levelSteps) {
|
|
538
|
+
await this.executeStep(levelStep, agentResolver);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Check if the target variable was set in this level
|
|
542
|
+
// For now, we'll assume the last saveAs in the level is the result
|
|
543
|
+
if (levelSteps.length > 0) {
|
|
544
|
+
const lastStep = levelSteps[levelSteps.length - 1];
|
|
545
|
+
if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
|
|
546
|
+
finalResult = this.context[lastStep.saveAs];
|
|
547
|
+
completedLevel = level.levelNumber;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
// Timed execution for this level
|
|
553
|
+
currentTimeout += level.timeout;
|
|
554
|
+
|
|
555
|
+
const timeoutPromise = new Promise(resolve => {
|
|
556
|
+
setTimeout(() => resolve({ timedOut: true }), level.timeout);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const levelPromise = (async () => {
|
|
560
|
+
const levelSteps = require('./parser').parseBlock(level.steps);
|
|
561
|
+
for (const levelStep of levelSteps) {
|
|
562
|
+
await this.executeStep(levelStep, agentResolver);
|
|
563
|
+
}
|
|
564
|
+
return { timedOut: false };
|
|
565
|
+
})();
|
|
566
|
+
|
|
567
|
+
const result = await Promise.race([timeoutPromise, levelPromise]);
|
|
568
|
+
|
|
569
|
+
if (!result.timedOut) {
|
|
570
|
+
// Level completed successfully
|
|
571
|
+
if (levelSteps && levelSteps.length > 0) {
|
|
572
|
+
const lastStep = levelSteps[levelSteps.length - 1];
|
|
573
|
+
if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
|
|
574
|
+
finalResult = this.context[lastStep.saveAs];
|
|
575
|
+
completedLevel = level.levelNumber;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// If timed out, continue to next level
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Set escalation status in context
|
|
585
|
+
this.context.escalation_completed = finalResult !== null;
|
|
586
|
+
this.context.timed_out = finalResult === null;
|
|
587
|
+
if (completedLevel !== null) {
|
|
588
|
+
this.context.escalation_level = completedLevel;
|
|
589
|
+
}
|
|
590
|
+
|
|
467
591
|
break;
|
|
468
592
|
}
|
|
469
593
|
|
|
@@ -478,26 +602,87 @@ class RuntimeAPI {
|
|
|
478
602
|
}
|
|
479
603
|
|
|
480
604
|
case 'debrief': {
|
|
605
|
+
// ✅ SEMANTIC VALIDATION: Check symbols in message
|
|
606
|
+
if (step.message.includes('{')) {
|
|
607
|
+
const symbols = step.message.match(/\{([^\}]+)\}/g) || [];
|
|
608
|
+
for (const symbolMatch of symbols) {
|
|
609
|
+
const symbol = symbolMatch.replace(/[{}]/g, '');
|
|
610
|
+
this._requireSemantic(symbol, 'debrief');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
481
613
|
this.emit('debrief', { agent: step.agent, message: step.message });
|
|
482
614
|
break;
|
|
483
615
|
}
|
|
484
616
|
|
|
485
|
-
// ✅
|
|
486
|
-
case '
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
617
|
+
// ✅ NEW: Prompt step handler
|
|
618
|
+
case 'prompt': {
|
|
619
|
+
if (this.verbose) {
|
|
620
|
+
console.log(`❓ Prompt: ${step.question}`);
|
|
621
|
+
}
|
|
622
|
+
// In non-interactive mode, leave as no-op
|
|
623
|
+
// (Could integrate with stdin or API in future)
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ✅ NEW: Emit step handler with semantic validation
|
|
628
|
+
case 'emit': {
|
|
629
|
+
// ✅ SEMANTIC VALIDATION: Check all symbols in payload
|
|
630
|
+
const payloadTemplate = step.payload;
|
|
631
|
+
const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
|
|
632
|
+
|
|
633
|
+
let shouldEmit = true;
|
|
634
|
+
for (const symbolMatch of symbols) {
|
|
635
|
+
const symbol = symbolMatch.replace(/[{}]/g, '');
|
|
636
|
+
if (!this._requireSemantic(symbol, 'emit')) {
|
|
637
|
+
shouldEmit = false;
|
|
638
|
+
// Continue to validate all symbols (for complete error reporting)
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (!shouldEmit) {
|
|
643
|
+
if (this.verbose) {
|
|
644
|
+
console.log(`⏭️ Skipped emit due to missing semantic symbols`);
|
|
645
|
+
}
|
|
490
646
|
break;
|
|
491
647
|
}
|
|
648
|
+
|
|
649
|
+
const payload = step.payload.replace(/\{([^\}]+)\}/g, (_, path) => {
|
|
650
|
+
const value = this.getNested(this.context, path.trim());
|
|
651
|
+
return value !== undefined ? String(value) : `{${path}}`;
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
this.emit(step.event, {
|
|
655
|
+
payload: payload,
|
|
656
|
+
workflow: this.context.workflow_name,
|
|
657
|
+
timestamp: new Date().toISOString()
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
if (this.verbose) {
|
|
661
|
+
console.log(`📤 Emit event "${step.event}" with payload: ${payload}`);
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
492
665
|
|
|
493
|
-
|
|
666
|
+
// ✅ File Persist step handler with semantic validation
|
|
667
|
+
case 'persist': {
|
|
668
|
+
// ✅ SEMANTIC VALIDATION: Require symbol exists
|
|
669
|
+
if (!this._requireSemantic(step.variable, 'persist')) {
|
|
670
|
+
// Default policy: Skip persist (safe)
|
|
671
|
+
if (this.verbose) {
|
|
672
|
+
console.log(`⏭️ Skipped persist for undefined "${step.variable}"`);
|
|
673
|
+
}
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const sourceValue = this.context[step.variable]; // Now guaranteed defined
|
|
678
|
+
const outputPath = path.resolve(process.cwd(), step.target);
|
|
494
679
|
const outputDir = path.dirname(outputPath);
|
|
495
680
|
if (!fs.existsSync(outputDir)) {
|
|
496
681
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
497
682
|
}
|
|
498
683
|
|
|
499
684
|
let content;
|
|
500
|
-
if (step.
|
|
685
|
+
if (step.target.endsWith('.json')) {
|
|
501
686
|
content = JSON.stringify(sourceValue, null, 2);
|
|
502
687
|
} else {
|
|
503
688
|
content = String(sourceValue);
|
|
@@ -506,23 +691,27 @@ class RuntimeAPI {
|
|
|
506
691
|
fs.writeFileSync(outputPath, content, 'utf8');
|
|
507
692
|
|
|
508
693
|
if (this.verbose) {
|
|
509
|
-
console.log(`💾 Persisted "${step.
|
|
694
|
+
console.log(`💾 Persisted "${step.variable}" to ${step.target}`);
|
|
510
695
|
}
|
|
511
696
|
break;
|
|
512
697
|
}
|
|
513
698
|
|
|
514
|
-
// ✅ NEW: Database persist handler
|
|
699
|
+
// ✅ NEW: Database persist handler with semantic validation
|
|
515
700
|
case 'persist-db': {
|
|
516
701
|
if (!this.dbClient) {
|
|
517
702
|
this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
|
|
518
703
|
break;
|
|
519
704
|
}
|
|
520
705
|
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
this.
|
|
706
|
+
// ✅ SEMANTIC VALIDATION: Require symbol exists
|
|
707
|
+
if (!this._requireSemantic(step.variable, 'persist-db')) {
|
|
708
|
+
if (this.verbose) {
|
|
709
|
+
console.log(`⏭️ Skipped DB persist for undefined "${step.variable}"`);
|
|
710
|
+
}
|
|
524
711
|
break;
|
|
525
712
|
}
|
|
713
|
+
|
|
714
|
+
const sourceValue = this.context[step.variable]; // Now guaranteed defined
|
|
526
715
|
|
|
527
716
|
try {
|
|
528
717
|
switch (this.dbClient.type) {
|
|
@@ -563,39 +752,31 @@ class RuntimeAPI {
|
|
|
563
752
|
}
|
|
564
753
|
|
|
565
754
|
if (this.verbose) {
|
|
566
|
-
console.log(`🗄️ Persisted "${step.
|
|
755
|
+
console.log(`🗄️ Persisted "${step.variable}" to DB collection ${step.collection}`);
|
|
567
756
|
}
|
|
568
757
|
} catch (e) {
|
|
569
|
-
this.addWarning(`DB persist failed for "${step.
|
|
758
|
+
this.addWarning(`DB persist failed for "${step.variable}": ${e.message}`);
|
|
570
759
|
}
|
|
571
760
|
break;
|
|
572
761
|
}
|
|
573
762
|
}
|
|
574
763
|
|
|
575
|
-
// ✅ ADDITION 5 — Security Warning for External Resolvers
|
|
576
764
|
if (this.verbose) {
|
|
577
|
-
for (const r of this.allowedResolvers) {
|
|
578
|
-
// Note: We can't easily check if resolvers are external here since we only have names
|
|
579
|
-
// This would need to be moved to where we have the actual resolver objects
|
|
580
|
-
}
|
|
581
765
|
console.log(`\n[Step: ${step.type} | saveAs: ${step.saveAs || 'N/A'}]`);
|
|
582
766
|
console.log(JSON.stringify(this.context, null, 2));
|
|
583
767
|
}
|
|
584
768
|
}
|
|
585
769
|
|
|
586
770
|
async executeWorkflow(workflow, inputs, agentResolver) {
|
|
587
|
-
// Handle regular workflows only (Evolve is a step type now)
|
|
588
771
|
if (workflow.type !== 'workflow') {
|
|
589
772
|
throw new Error(`Unknown workflow type: ${workflow.type}`);
|
|
590
773
|
}
|
|
591
774
|
|
|
592
|
-
// ✅ Inject workflow name into context
|
|
593
775
|
this.context = {
|
|
594
776
|
...inputs,
|
|
595
777
|
workflow_name: workflow.name
|
|
596
778
|
};
|
|
597
779
|
|
|
598
|
-
// ✅ Check generation constraint from Constraint: max_generations = X
|
|
599
780
|
const currentGeneration = inputs.__generation || 1;
|
|
600
781
|
if (workflow.maxGenerations !== null && currentGeneration > workflow.maxGenerations) {
|
|
601
782
|
throw new Error(`Workflow generation ${currentGeneration} exceeds Constraint: max_generations = ${workflow.maxGenerations}`);
|
|
@@ -626,9 +807,13 @@ class RuntimeAPI {
|
|
|
626
807
|
});
|
|
627
808
|
}
|
|
628
809
|
|
|
810
|
+
// ✅ SEMANTIC VALIDATION: For return values
|
|
629
811
|
const result = {};
|
|
630
812
|
for (const key of workflow.returnValues) {
|
|
631
|
-
|
|
813
|
+
if (this._requireSemantic(key, 'return')) {
|
|
814
|
+
result[key] = this.context[key];
|
|
815
|
+
}
|
|
816
|
+
// Skip undefined return values (safe default)
|
|
632
817
|
}
|
|
633
818
|
return result;
|
|
634
819
|
}
|