openagentflow 0.1.0
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/LICENSE +21 -0
- package/README.md +262 -0
- package/adapters/langgraph/demo_template.js +104 -0
- package/adapters/langgraph/index.js +363 -0
- package/adapters/langgraph/templates.js +502 -0
- package/cli/env.js +193 -0
- package/cli/index.js +580 -0
- package/compiler/compiler.js +134 -0
- package/compiler/index.js +7 -0
- package/compiler/ir-generator.js +142 -0
- package/compiler/validator.js +594 -0
- package/examples/hello.oaf +14 -0
- package/examples/software-dev.oaf +60 -0
- package/examples/summarize-input.json +3 -0
- package/examples/summarize.oaf +28 -0
- package/examples/support-triage-input.json +3 -0
- package/examples/support-triage.oaf +49 -0
- package/package.json +44 -0
- package/parser/ast.js +240 -0
- package/parser/index.js +9 -0
- package/parser/lexer.js +376 -0
- package/parser/parser.js +505 -0
package/parser/lexer.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAgentFlow Lexer
|
|
3
|
+
*
|
|
4
|
+
* Performs lexical analysis on .oaf source text, producing a stream of tokens.
|
|
5
|
+
* This is the first stage of the compilation pipeline.
|
|
6
|
+
*
|
|
7
|
+
* Pipeline: Source Text → [Lexer] → Tokens → Parser → AST
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ─── Token Types ───────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export const TokenType = Object.freeze({
|
|
13
|
+
// Keywords
|
|
14
|
+
WORKFLOW: 'WORKFLOW',
|
|
15
|
+
AGENT: 'AGENT',
|
|
16
|
+
STATE: 'STATE',
|
|
17
|
+
FLOW: 'FLOW',
|
|
18
|
+
CONFIG: 'CONFIG',
|
|
19
|
+
START: 'START',
|
|
20
|
+
END: 'END',
|
|
21
|
+
|
|
22
|
+
// Type keywords
|
|
23
|
+
STRING_TYPE: 'STRING_TYPE',
|
|
24
|
+
INT_TYPE: 'INT_TYPE',
|
|
25
|
+
FLOAT_TYPE: 'FLOAT_TYPE',
|
|
26
|
+
BOOL_TYPE: 'BOOL_TYPE',
|
|
27
|
+
LIST_TYPE: 'LIST_TYPE',
|
|
28
|
+
MAP_TYPE: 'MAP_TYPE',
|
|
29
|
+
|
|
30
|
+
// Boolean literals
|
|
31
|
+
TRUE: 'TRUE',
|
|
32
|
+
FALSE: 'FALSE',
|
|
33
|
+
|
|
34
|
+
// Literals
|
|
35
|
+
STRING: 'STRING',
|
|
36
|
+
TRIPLE_STRING: 'TRIPLE_STRING',
|
|
37
|
+
INTEGER: 'INTEGER',
|
|
38
|
+
FLOAT: 'FLOAT',
|
|
39
|
+
IDENTIFIER: 'IDENTIFIER',
|
|
40
|
+
|
|
41
|
+
// Punctuation
|
|
42
|
+
LBRACE: 'LBRACE', // {
|
|
43
|
+
RBRACE: 'RBRACE', // }
|
|
44
|
+
LBRACKET: 'LBRACKET', // [
|
|
45
|
+
RBRACKET: 'RBRACKET', // ]
|
|
46
|
+
LPAREN: 'LPAREN', // (
|
|
47
|
+
RPAREN: 'RPAREN', // )
|
|
48
|
+
COLON: 'COLON', // :
|
|
49
|
+
COMMA: 'COMMA', // ,
|
|
50
|
+
ARROW: 'ARROW', // ->
|
|
51
|
+
AT: 'AT', // @
|
|
52
|
+
|
|
53
|
+
// Special
|
|
54
|
+
EOF: 'EOF',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const KEYWORDS = new Map([
|
|
58
|
+
['workflow', TokenType.WORKFLOW],
|
|
59
|
+
['agent', TokenType.AGENT],
|
|
60
|
+
['state', TokenType.STATE],
|
|
61
|
+
['flow', TokenType.FLOW],
|
|
62
|
+
['config', TokenType.CONFIG],
|
|
63
|
+
['start', TokenType.START],
|
|
64
|
+
['end', TokenType.END],
|
|
65
|
+
['string', TokenType.STRING_TYPE],
|
|
66
|
+
['int', TokenType.INT_TYPE],
|
|
67
|
+
['float', TokenType.FLOAT_TYPE],
|
|
68
|
+
['bool', TokenType.BOOL_TYPE],
|
|
69
|
+
['list', TokenType.LIST_TYPE],
|
|
70
|
+
['map', TokenType.MAP_TYPE],
|
|
71
|
+
['true', TokenType.TRUE],
|
|
72
|
+
['false', TokenType.FALSE],
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// ─── Token ─────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export class Token {
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} type - One of TokenType values
|
|
80
|
+
* @param {string} value - Raw lexeme
|
|
81
|
+
* @param {number} line - 1-based line number
|
|
82
|
+
* @param {number} column - 1-based column number
|
|
83
|
+
*/
|
|
84
|
+
constructor(type, value, line, column) {
|
|
85
|
+
this.type = type;
|
|
86
|
+
this.value = value;
|
|
87
|
+
this.line = line;
|
|
88
|
+
this.column = column;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
toString() {
|
|
92
|
+
return `Token(${this.type}, ${JSON.stringify(this.value)}, ${this.line}:${this.column})`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Lexer Error ───────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export class LexerError extends Error {
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} message
|
|
101
|
+
* @param {number} line
|
|
102
|
+
* @param {number} column
|
|
103
|
+
*/
|
|
104
|
+
constructor(message, line, column) {
|
|
105
|
+
super(`[ERROR] ${line}:${column} — ${message}`);
|
|
106
|
+
this.name = 'LexerError';
|
|
107
|
+
this.line = line;
|
|
108
|
+
this.column = column;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Lexer ─────────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
export class Lexer {
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} source - The .oaf source text
|
|
117
|
+
* @param {string} [filename='<input>'] - Filename for diagnostics
|
|
118
|
+
*/
|
|
119
|
+
constructor(source, filename = '<input>') {
|
|
120
|
+
this.source = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
121
|
+
this.filename = filename;
|
|
122
|
+
this.pos = 0;
|
|
123
|
+
this.line = 1;
|
|
124
|
+
this.column = 1;
|
|
125
|
+
this.tokens = [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Tokenize the entire source and return the token array.
|
|
130
|
+
* @returns {Token[]}
|
|
131
|
+
*/
|
|
132
|
+
tokenize() {
|
|
133
|
+
while (this.pos < this.source.length) {
|
|
134
|
+
this.skipWhitespaceAndComments();
|
|
135
|
+
if (this.pos >= this.source.length) break;
|
|
136
|
+
|
|
137
|
+
const ch = this.source[this.pos];
|
|
138
|
+
|
|
139
|
+
// Triple-quoted string
|
|
140
|
+
if (ch === '"' && this.source.substring(this.pos, this.pos + 3) === '"""') {
|
|
141
|
+
this.readTripleString();
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// String literal
|
|
146
|
+
if (ch === '"') {
|
|
147
|
+
this.readString();
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Arrow ->
|
|
152
|
+
if (ch === '-' && this.peek(1) === '>') {
|
|
153
|
+
this.tokens.push(new Token(TokenType.ARROW, '->', this.line, this.column));
|
|
154
|
+
this.advance(2);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Number (integer or float)
|
|
159
|
+
if (ch === '-' && this.isDigit(this.peek(1))) {
|
|
160
|
+
this.readNumber();
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (this.isDigit(ch)) {
|
|
164
|
+
this.readNumber();
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Identifier or keyword
|
|
169
|
+
if (this.isIdentStart(ch)) {
|
|
170
|
+
this.readIdentifier();
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Single-character punctuation
|
|
175
|
+
const punctMap = {
|
|
176
|
+
'{': TokenType.LBRACE,
|
|
177
|
+
'}': TokenType.RBRACE,
|
|
178
|
+
'[': TokenType.LBRACKET,
|
|
179
|
+
']': TokenType.RBRACKET,
|
|
180
|
+
'(': TokenType.LPAREN,
|
|
181
|
+
')': TokenType.RPAREN,
|
|
182
|
+
':': TokenType.COLON,
|
|
183
|
+
',': TokenType.COMMA,
|
|
184
|
+
'@': TokenType.AT,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (punctMap[ch]) {
|
|
188
|
+
this.tokens.push(new Token(punctMap[ch], ch, this.line, this.column));
|
|
189
|
+
this.advance(1);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw new LexerError(`Unexpected character: '${ch}'`, this.line, this.column);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.tokens.push(new Token(TokenType.EOF, '', this.line, this.column));
|
|
197
|
+
return this.tokens;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
peek(offset = 0) {
|
|
203
|
+
const idx = this.pos + offset;
|
|
204
|
+
return idx < this.source.length ? this.source[idx] : '\0';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
advance(count = 1) {
|
|
208
|
+
for (let i = 0; i < count; i++) {
|
|
209
|
+
if (this.source[this.pos] === '\n') {
|
|
210
|
+
this.line++;
|
|
211
|
+
this.column = 1;
|
|
212
|
+
} else {
|
|
213
|
+
this.column++;
|
|
214
|
+
}
|
|
215
|
+
this.pos++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
isDigit(ch) {
|
|
220
|
+
return ch >= '0' && ch <= '9';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
isIdentStart(ch) {
|
|
224
|
+
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
isIdentPart(ch) {
|
|
228
|
+
if (ch === '-' && this.peek(1) !== '>') {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
return this.isIdentStart(ch) || this.isDigit(ch);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
skipWhitespaceAndComments() {
|
|
235
|
+
while (this.pos < this.source.length) {
|
|
236
|
+
const ch = this.source[this.pos];
|
|
237
|
+
|
|
238
|
+
// Whitespace
|
|
239
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
240
|
+
this.advance(1);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Line comment
|
|
245
|
+
if (ch === '/' && this.peek(1) === '/') {
|
|
246
|
+
while (this.pos < this.source.length && this.source[this.pos] !== '\n') {
|
|
247
|
+
this.advance(1);
|
|
248
|
+
}
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
readString() {
|
|
257
|
+
const startLine = this.line;
|
|
258
|
+
const startCol = this.column;
|
|
259
|
+
this.advance(1); // skip opening "
|
|
260
|
+
|
|
261
|
+
let value = '';
|
|
262
|
+
while (this.pos < this.source.length && this.source[this.pos] !== '"') {
|
|
263
|
+
if (this.source[this.pos] === '\\') {
|
|
264
|
+
this.advance(1);
|
|
265
|
+
const escaped = this.source[this.pos];
|
|
266
|
+
const escapeMap = { 'n': '\n', 't': '\t', '"': '"', '\\': '\\' };
|
|
267
|
+
value += escapeMap[escaped] ?? escaped;
|
|
268
|
+
} else {
|
|
269
|
+
value += this.source[this.pos];
|
|
270
|
+
}
|
|
271
|
+
this.advance(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (this.pos >= this.source.length) {
|
|
275
|
+
throw new LexerError('Unterminated string literal', startLine, startCol);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.advance(1); // skip closing "
|
|
279
|
+
this.tokens.push(new Token(TokenType.STRING, value, startLine, startCol));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
readTripleString() {
|
|
283
|
+
const startLine = this.line;
|
|
284
|
+
const startCol = this.column;
|
|
285
|
+
this.advance(3); // skip opening """
|
|
286
|
+
|
|
287
|
+
let value = '';
|
|
288
|
+
while (this.pos < this.source.length) {
|
|
289
|
+
if (this.source.substring(this.pos, this.pos + 3) === '"""') {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
value += this.source[this.pos];
|
|
293
|
+
this.advance(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (this.pos >= this.source.length) {
|
|
297
|
+
throw new LexerError('Unterminated triple-quoted string', startLine, startCol);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.advance(3); // skip closing """
|
|
301
|
+
|
|
302
|
+
// Dedent the triple-quoted string
|
|
303
|
+
value = this.dedent(value);
|
|
304
|
+
this.tokens.push(new Token(TokenType.TRIPLE_STRING, value, startLine, startCol));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Strip common leading whitespace from a multi-line string (Python-style dedent).
|
|
309
|
+
*/
|
|
310
|
+
dedent(text) {
|
|
311
|
+
const lines = text.split('\n');
|
|
312
|
+
|
|
313
|
+
// Remove leading/trailing blank lines
|
|
314
|
+
while (lines.length > 0 && lines[0].trim() === '') lines.shift();
|
|
315
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop();
|
|
316
|
+
|
|
317
|
+
if (lines.length === 0) return '';
|
|
318
|
+
|
|
319
|
+
// Find minimum indent (ignoring blank lines)
|
|
320
|
+
let minIndent = Infinity;
|
|
321
|
+
for (const line of lines) {
|
|
322
|
+
if (line.trim() === '') continue;
|
|
323
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
324
|
+
if (indent < minIndent) minIndent = indent;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (minIndent === Infinity) minIndent = 0;
|
|
328
|
+
|
|
329
|
+
return lines.map(l => l.slice(minIndent)).join('\n');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
readNumber() {
|
|
333
|
+
const startLine = this.line;
|
|
334
|
+
const startCol = this.column;
|
|
335
|
+
let numStr = '';
|
|
336
|
+
let isFloat = false;
|
|
337
|
+
|
|
338
|
+
if (this.source[this.pos] === '-') {
|
|
339
|
+
numStr += '-';
|
|
340
|
+
this.advance(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
while (this.pos < this.source.length && this.isDigit(this.source[this.pos])) {
|
|
344
|
+
numStr += this.source[this.pos];
|
|
345
|
+
this.advance(1);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (this.pos < this.source.length && this.source[this.pos] === '.' && this.isDigit(this.peek(1))) {
|
|
349
|
+
isFloat = true;
|
|
350
|
+
numStr += '.';
|
|
351
|
+
this.advance(1);
|
|
352
|
+
while (this.pos < this.source.length && this.isDigit(this.source[this.pos])) {
|
|
353
|
+
numStr += this.source[this.pos];
|
|
354
|
+
this.advance(1);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const type = isFloat ? TokenType.FLOAT : TokenType.INTEGER;
|
|
359
|
+
this.tokens.push(new Token(type, numStr, startLine, startCol));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
readIdentifier() {
|
|
363
|
+
const startLine = this.line;
|
|
364
|
+
const startCol = this.column;
|
|
365
|
+
let ident = '';
|
|
366
|
+
|
|
367
|
+
while (this.pos < this.source.length && this.isIdentPart(this.source[this.pos])) {
|
|
368
|
+
ident += this.source[this.pos];
|
|
369
|
+
this.advance(1);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const keywordType = KEYWORDS.get(ident);
|
|
373
|
+
const type = keywordType ?? TokenType.IDENTIFIER;
|
|
374
|
+
this.tokens.push(new Token(type, ident, startLine, startCol));
|
|
375
|
+
}
|
|
376
|
+
}
|