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/parser.js
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAgentFlow Parser
|
|
3
|
+
*
|
|
4
|
+
* Recursive-descent parser that consumes a token stream from the Lexer
|
|
5
|
+
* and produces an Abstract Syntax Tree (AST).
|
|
6
|
+
*
|
|
7
|
+
* Pipeline: Tokens → [Parser] → AST → Semantic Validator
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { TokenType } from './lexer.js';
|
|
11
|
+
import {
|
|
12
|
+
Program,
|
|
13
|
+
WorkflowDecl,
|
|
14
|
+
StateBlock,
|
|
15
|
+
StateField,
|
|
16
|
+
StateOption,
|
|
17
|
+
PrimitiveType,
|
|
18
|
+
ListType,
|
|
19
|
+
MapType,
|
|
20
|
+
AgentBlock,
|
|
21
|
+
FlowBlock,
|
|
22
|
+
Edge,
|
|
23
|
+
ConfigBlock,
|
|
24
|
+
ConfigEntry,
|
|
25
|
+
} from './ast.js';
|
|
26
|
+
|
|
27
|
+
// ─── Parser Error ──────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export class ParseError extends Error {
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} message
|
|
32
|
+
* @param {import('./lexer.js').Token} token
|
|
33
|
+
*/
|
|
34
|
+
constructor(message, token) {
|
|
35
|
+
const loc = token ? `${token.line}:${token.column}` : '?:?';
|
|
36
|
+
super(`[ERROR] ${loc} — ${message}`);
|
|
37
|
+
this.name = 'ParseError';
|
|
38
|
+
this.token = token;
|
|
39
|
+
this.line = token?.line;
|
|
40
|
+
this.column = token?.column;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Parser ────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export class Parser {
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('./lexer.js').Token[]} tokens
|
|
49
|
+
*/
|
|
50
|
+
constructor(tokens) {
|
|
51
|
+
this.tokens = tokens;
|
|
52
|
+
this.pos = 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Token Navigation ────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
current() {
|
|
58
|
+
return this.tokens[this.pos];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
peek(offset = 1) {
|
|
62
|
+
return this.tokens[this.pos + offset] ?? this.tokens[this.tokens.length - 1];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
advance() {
|
|
66
|
+
const token = this.tokens[this.pos];
|
|
67
|
+
this.pos++;
|
|
68
|
+
return token;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Expect the current token to match a given type, consume and return it.
|
|
73
|
+
* @param {string} type
|
|
74
|
+
* @param {string} [contextMsg]
|
|
75
|
+
* @returns {import('./lexer.js').Token}
|
|
76
|
+
*/
|
|
77
|
+
expect(type, contextMsg = '') {
|
|
78
|
+
const token = this.current();
|
|
79
|
+
if (token.type !== type) {
|
|
80
|
+
const ctx = contextMsg ? ` (${contextMsg})` : '';
|
|
81
|
+
throw new ParseError(
|
|
82
|
+
`Expected ${type} but found ${token.type} "${token.value}"${ctx}`,
|
|
83
|
+
token
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return this.advance();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if the current token matches a type without consuming it.
|
|
91
|
+
* @param {string} type
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
check(type) {
|
|
95
|
+
return this.current().type === type;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* If the current token matches, consume and return it; otherwise return null.
|
|
100
|
+
* @param {string} type
|
|
101
|
+
* @returns {import('./lexer.js').Token|null}
|
|
102
|
+
*/
|
|
103
|
+
match(type) {
|
|
104
|
+
if (this.check(type)) return this.advance();
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Entry Point ─────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse the token stream into a Program AST.
|
|
112
|
+
* @returns {Program}
|
|
113
|
+
*/
|
|
114
|
+
parse() {
|
|
115
|
+
const workflow = this.parseWorkflow();
|
|
116
|
+
this.expect(TokenType.EOF, 'expected end of file');
|
|
117
|
+
return new Program(workflow);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Workflow ────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
parseWorkflow() {
|
|
123
|
+
const kwToken = this.expect(TokenType.WORKFLOW, 'expected "workflow"');
|
|
124
|
+
const nameToken = this.expect(TokenType.STRING, 'expected workflow name string');
|
|
125
|
+
this.expect(TokenType.LBRACE, 'expected "{"');
|
|
126
|
+
|
|
127
|
+
let state = null;
|
|
128
|
+
const agents = [];
|
|
129
|
+
let flow = null;
|
|
130
|
+
let config = null;
|
|
131
|
+
|
|
132
|
+
while (!this.check(TokenType.RBRACE) && !this.check(TokenType.EOF)) {
|
|
133
|
+
const token = this.current();
|
|
134
|
+
|
|
135
|
+
switch (token.type) {
|
|
136
|
+
case TokenType.STATE:
|
|
137
|
+
if (state !== null) {
|
|
138
|
+
throw new ParseError('Multiple state blocks declared', token);
|
|
139
|
+
}
|
|
140
|
+
state = this.parseStateBlock();
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case TokenType.AGENT:
|
|
144
|
+
agents.push(this.parseAgentBlock());
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
case TokenType.FLOW:
|
|
148
|
+
if (flow !== null) {
|
|
149
|
+
throw new ParseError('Multiple flow blocks declared', token);
|
|
150
|
+
}
|
|
151
|
+
flow = this.parseFlowBlock();
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case TokenType.CONFIG:
|
|
155
|
+
if (config !== null) {
|
|
156
|
+
throw new ParseError('Multiple config blocks declared', token);
|
|
157
|
+
}
|
|
158
|
+
config = this.parseConfigBlock();
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
default:
|
|
162
|
+
throw new ParseError(
|
|
163
|
+
`Unexpected token "${token.value}" in workflow body`,
|
|
164
|
+
token
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.expect(TokenType.RBRACE, 'expected "}" to close workflow');
|
|
170
|
+
|
|
171
|
+
return new WorkflowDecl(
|
|
172
|
+
nameToken.value,
|
|
173
|
+
state,
|
|
174
|
+
agents,
|
|
175
|
+
flow,
|
|
176
|
+
config,
|
|
177
|
+
kwToken.line,
|
|
178
|
+
kwToken.column
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── State Block ─────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
parseStateBlock() {
|
|
185
|
+
const kwToken = this.expect(TokenType.STATE);
|
|
186
|
+
this.expect(TokenType.LBRACE, 'expected "{" after "state"');
|
|
187
|
+
|
|
188
|
+
const fields = [];
|
|
189
|
+
while (!this.check(TokenType.RBRACE) && !this.check(TokenType.EOF)) {
|
|
190
|
+
const nameToken = this.expect(TokenType.IDENTIFIER, 'expected state variable name');
|
|
191
|
+
this.expect(TokenType.COLON, 'expected ":" after variable name');
|
|
192
|
+
const typeExpr = this.parseTypeExpr();
|
|
193
|
+
const options = this.parseStateOptions();
|
|
194
|
+
fields.push(new StateField(nameToken.value, typeExpr, options, nameToken.line, nameToken.column));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.expect(TokenType.RBRACE, 'expected "}" to close state block');
|
|
198
|
+
return new StateBlock(fields, kwToken.line, kwToken.column);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
parseStateOptions() {
|
|
202
|
+
const options = [];
|
|
203
|
+
while (this.check(TokenType.AT)) {
|
|
204
|
+
const atToken = this.advance(); // consume @
|
|
205
|
+
const nameToken = this.expect(TokenType.IDENTIFIER, 'expected option name after "@"');
|
|
206
|
+
let args = [];
|
|
207
|
+
if (this.check(TokenType.LPAREN)) {
|
|
208
|
+
args = this.parseOptionArgs();
|
|
209
|
+
}
|
|
210
|
+
options.push(new StateOption(nameToken.value, args, atToken.line, atToken.column));
|
|
211
|
+
}
|
|
212
|
+
return options;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
parseOptionArgs() {
|
|
216
|
+
this.expect(TokenType.LPAREN, 'expected "(" after option name');
|
|
217
|
+
const args = [];
|
|
218
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
219
|
+
args.push(this.parseOptionArg());
|
|
220
|
+
while (this.match(TokenType.COMMA)) {
|
|
221
|
+
if (this.check(TokenType.RPAREN)) break; // trailing comma
|
|
222
|
+
args.push(this.parseOptionArg());
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
this.expect(TokenType.RPAREN, 'expected ")" after option arguments');
|
|
226
|
+
return args;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
parseOptionArg() {
|
|
230
|
+
const token = this.current();
|
|
231
|
+
if (token.type === TokenType.STRING || token.type === TokenType.TRIPLE_STRING) {
|
|
232
|
+
return this.advance().value;
|
|
233
|
+
}
|
|
234
|
+
if (token.type === TokenType.INTEGER) {
|
|
235
|
+
return parseInt(this.advance().value, 10);
|
|
236
|
+
}
|
|
237
|
+
if (token.type === TokenType.FLOAT) {
|
|
238
|
+
return parseFloat(this.advance().value);
|
|
239
|
+
}
|
|
240
|
+
if (token.type === TokenType.TRUE) {
|
|
241
|
+
this.advance();
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
if (token.type === TokenType.FALSE) {
|
|
245
|
+
this.advance();
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
if (
|
|
249
|
+
token.type === TokenType.IDENTIFIER ||
|
|
250
|
+
token.type === TokenType.STRING ||
|
|
251
|
+
token.type === TokenType.INTEGER ||
|
|
252
|
+
token.type === TokenType.FLOAT ||
|
|
253
|
+
token.type === TokenType.LIST_TYPE ||
|
|
254
|
+
token.type === TokenType.MAP_TYPE
|
|
255
|
+
) {
|
|
256
|
+
return this.advance().value;
|
|
257
|
+
}
|
|
258
|
+
throw new ParseError(`Expected option argument, found "${token.value}"`, token);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Type Expressions ────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
parseTypeExpr() {
|
|
264
|
+
const token = this.current();
|
|
265
|
+
|
|
266
|
+
// list[T]
|
|
267
|
+
if (token.type === TokenType.LIST_TYPE) {
|
|
268
|
+
return this.parseListType();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// map[K, V]
|
|
272
|
+
if (token.type === TokenType.MAP_TYPE) {
|
|
273
|
+
return this.parseMapType();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Primitive types
|
|
277
|
+
const primitiveTypes = [TokenType.STRING_TYPE, TokenType.INT_TYPE, TokenType.FLOAT_TYPE, TokenType.BOOL_TYPE];
|
|
278
|
+
if (primitiveTypes.includes(token.type)) {
|
|
279
|
+
this.advance();
|
|
280
|
+
return new PrimitiveType(token.value, token.line, token.column);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
throw new ParseError(`Expected type expression, found "${token.value}"`, token);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
parseListType() {
|
|
287
|
+
const kwToken = this.expect(TokenType.LIST_TYPE);
|
|
288
|
+
this.expect(TokenType.LBRACKET, 'expected "[" after "list"');
|
|
289
|
+
const elementType = this.parseTypeExpr();
|
|
290
|
+
this.expect(TokenType.RBRACKET, 'expected "]" to close list type');
|
|
291
|
+
return new ListType(elementType, kwToken.line, kwToken.column);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
parseMapType() {
|
|
295
|
+
const kwToken = this.expect(TokenType.MAP_TYPE);
|
|
296
|
+
this.expect(TokenType.LBRACKET, 'expected "[" after "map"');
|
|
297
|
+
const keyType = this.parseTypeExpr();
|
|
298
|
+
this.expect(TokenType.COMMA, 'expected "," between map key and value types');
|
|
299
|
+
const valueType = this.parseTypeExpr();
|
|
300
|
+
this.expect(TokenType.RBRACKET, 'expected "]" to close map type');
|
|
301
|
+
return new MapType(keyType, valueType, kwToken.line, kwToken.column);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Agent Block ─────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
parseAgentBlock() {
|
|
307
|
+
const kwToken = this.expect(TokenType.AGENT);
|
|
308
|
+
const idToken = this.expect(TokenType.IDENTIFIER, 'expected agent identifier');
|
|
309
|
+
this.expect(TokenType.LBRACE, 'expected "{" after agent identifier');
|
|
310
|
+
|
|
311
|
+
const properties = {};
|
|
312
|
+
const seenProps = new Set();
|
|
313
|
+
|
|
314
|
+
while (!this.check(TokenType.RBRACE) && !this.check(TokenType.EOF)) {
|
|
315
|
+
const propToken = this.expect(TokenType.IDENTIFIER, 'expected property name');
|
|
316
|
+
this.expect(TokenType.COLON, `expected ":" after "${propToken.value}"`);
|
|
317
|
+
|
|
318
|
+
if (seenProps.has(propToken.value)) {
|
|
319
|
+
throw new ParseError(`Duplicate property "${propToken.value}" in agent "${idToken.value}"`, propToken);
|
|
320
|
+
}
|
|
321
|
+
seenProps.add(propToken.value);
|
|
322
|
+
|
|
323
|
+
switch (propToken.value) {
|
|
324
|
+
case 'instructions': {
|
|
325
|
+
const val = this.parseStringValue();
|
|
326
|
+
if (val.trim() === '') {
|
|
327
|
+
throw new ParseError(`Agent "${idToken.value}" instructions cannot be empty string`, propToken);
|
|
328
|
+
}
|
|
329
|
+
properties.instructions = val;
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case 'model': {
|
|
333
|
+
const val = this.expect(TokenType.STRING, 'expected model string').value;
|
|
334
|
+
if (val.trim() === '') {
|
|
335
|
+
throw new ParseError(`Agent "${idToken.value}" model cannot be empty string`, propToken);
|
|
336
|
+
}
|
|
337
|
+
properties.model = val;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
case 'provider': {
|
|
341
|
+
const val = this.expect(TokenType.STRING, 'expected provider string ("gemini", "openai", or "anthropic")').value;
|
|
342
|
+
if (val.trim() === '') {
|
|
343
|
+
throw new ParseError(`Agent "${idToken.value}" provider cannot be empty string`, propToken);
|
|
344
|
+
}
|
|
345
|
+
properties.provider = val;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case 'temperature': {
|
|
349
|
+
const numToken = this.current();
|
|
350
|
+
if (numToken.type === TokenType.FLOAT || numToken.type === TokenType.INTEGER) {
|
|
351
|
+
properties.temperature = parseFloat(this.advance().value);
|
|
352
|
+
} else {
|
|
353
|
+
throw new ParseError('Expected numeric value for temperature', numToken);
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case 'tools':
|
|
358
|
+
properties.tools = this.parseStringList();
|
|
359
|
+
break;
|
|
360
|
+
case 'inputs':
|
|
361
|
+
properties.inputs = this.parseIdentList();
|
|
362
|
+
break;
|
|
363
|
+
case 'outputs':
|
|
364
|
+
properties.outputs = this.parseIdentList();
|
|
365
|
+
break;
|
|
366
|
+
default:
|
|
367
|
+
throw new ParseError(`Unknown agent property: "${propToken.value}"`, propToken);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.expect(TokenType.RBRACE, 'expected "}" to close agent block');
|
|
372
|
+
|
|
373
|
+
if (!properties.instructions) {
|
|
374
|
+
throw new ParseError(`Agent "${idToken.value}" is missing required "instructions" property`, idToken);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return new AgentBlock(idToken.value, properties, kwToken.line, kwToken.column);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
parseStringValue() {
|
|
381
|
+
const token = this.current();
|
|
382
|
+
if (token.type === TokenType.STRING || token.type === TokenType.TRIPLE_STRING) {
|
|
383
|
+
return this.advance().value;
|
|
384
|
+
}
|
|
385
|
+
throw new ParseError('Expected string value', token);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
parseStringList() {
|
|
389
|
+
this.expect(TokenType.LBRACKET, 'expected "["');
|
|
390
|
+
const items = [];
|
|
391
|
+
|
|
392
|
+
if (!this.check(TokenType.RBRACKET)) {
|
|
393
|
+
items.push(this.expect(TokenType.STRING, 'expected string in list').value);
|
|
394
|
+
while (this.match(TokenType.COMMA)) {
|
|
395
|
+
if (this.check(TokenType.RBRACKET)) break; // trailing comma
|
|
396
|
+
items.push(this.expect(TokenType.STRING, 'expected string in list').value);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
this.expect(TokenType.RBRACKET, 'expected "]"');
|
|
401
|
+
return items;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
parseIdentList() {
|
|
405
|
+
this.expect(TokenType.LBRACKET, 'expected "["');
|
|
406
|
+
const items = [];
|
|
407
|
+
|
|
408
|
+
if (!this.check(TokenType.RBRACKET)) {
|
|
409
|
+
items.push(this.expect(TokenType.IDENTIFIER, 'expected identifier in list').value);
|
|
410
|
+
while (this.match(TokenType.COMMA)) {
|
|
411
|
+
if (this.check(TokenType.RBRACKET)) break; // trailing comma
|
|
412
|
+
items.push(this.expect(TokenType.IDENTIFIER, 'expected identifier in list').value);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
this.expect(TokenType.RBRACKET, 'expected "]"');
|
|
417
|
+
return items;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Flow Block ──────────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
parseFlowBlock() {
|
|
423
|
+
const kwToken = this.expect(TokenType.FLOW);
|
|
424
|
+
this.expect(TokenType.LBRACE, 'expected "{" after "flow"');
|
|
425
|
+
|
|
426
|
+
const edges = [];
|
|
427
|
+
while (!this.check(TokenType.RBRACE) && !this.check(TokenType.EOF)) {
|
|
428
|
+
const sourceToken = this.current();
|
|
429
|
+
const source = this.parseFlowNode('source');
|
|
430
|
+
this.expect(TokenType.ARROW, 'expected "->" in flow edge');
|
|
431
|
+
const target = this.parseFlowNode('target');
|
|
432
|
+
edges.push(new Edge(source, target, sourceToken.line, sourceToken.column));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
this.expect(TokenType.RBRACE, 'expected "}" to close flow block');
|
|
436
|
+
return new FlowBlock(edges, kwToken.line, kwToken.column);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Parse a flow node reference: an identifier, 'start', or 'end'.
|
|
441
|
+
*/
|
|
442
|
+
parseFlowNode(role) {
|
|
443
|
+
const token = this.current();
|
|
444
|
+
|
|
445
|
+
if (token.type === TokenType.START) {
|
|
446
|
+
this.advance();
|
|
447
|
+
return 'start';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (token.type === TokenType.END) {
|
|
451
|
+
this.advance();
|
|
452
|
+
return 'end';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (token.type === TokenType.IDENTIFIER) {
|
|
456
|
+
return this.advance().value;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
throw new ParseError(`Expected agent identifier, "start", or "end" as flow ${role}`, token);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Config Block ────────────────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
parseConfigBlock() {
|
|
465
|
+
const kwToken = this.expect(TokenType.CONFIG);
|
|
466
|
+
this.expect(TokenType.LBRACE, 'expected "{" after "config"');
|
|
467
|
+
|
|
468
|
+
const entries = [];
|
|
469
|
+
const seenKeys = new Set();
|
|
470
|
+
while (!this.check(TokenType.RBRACE) && !this.check(TokenType.EOF)) {
|
|
471
|
+
const keyToken = this.expect(TokenType.IDENTIFIER, 'expected config key');
|
|
472
|
+
if (seenKeys.has(keyToken.value)) {
|
|
473
|
+
throw new ParseError(`Duplicate configuration key "${keyToken.value}"`, keyToken);
|
|
474
|
+
}
|
|
475
|
+
seenKeys.add(keyToken.value);
|
|
476
|
+
this.expect(TokenType.COLON, `expected ":" after config key "${keyToken.value}"`);
|
|
477
|
+
const value = this.parseConfigValue();
|
|
478
|
+
entries.push(new ConfigEntry(keyToken.value, value, keyToken.line, keyToken.column));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this.expect(TokenType.RBRACE, 'expected "}" to close config block');
|
|
482
|
+
return new ConfigBlock(entries, kwToken.line, kwToken.column);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
parseConfigValue() {
|
|
486
|
+
const token = this.current();
|
|
487
|
+
|
|
488
|
+
switch (token.type) {
|
|
489
|
+
case TokenType.STRING:
|
|
490
|
+
return this.advance().value;
|
|
491
|
+
case TokenType.INTEGER:
|
|
492
|
+
return parseInt(this.advance().value, 10);
|
|
493
|
+
case TokenType.FLOAT:
|
|
494
|
+
return parseFloat(this.advance().value);
|
|
495
|
+
case TokenType.TRUE:
|
|
496
|
+
this.advance();
|
|
497
|
+
return true;
|
|
498
|
+
case TokenType.FALSE:
|
|
499
|
+
this.advance();
|
|
500
|
+
return false;
|
|
501
|
+
default:
|
|
502
|
+
throw new ParseError(`Expected config value, found "${token.value}"`, token);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|