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
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAgentFlow Semantic Validator
|
|
3
|
+
*
|
|
4
|
+
* Performs semantic analysis on the AST in three phases:
|
|
5
|
+
* Phase 1: Symbol Resolution (duplicates, cardinality, reserved words)
|
|
6
|
+
* Phase 2: Reference Validation (flow refs, input/output refs, type checks)
|
|
7
|
+
* Phase 3: Graph Validation (start/end, reachability, cycles)
|
|
8
|
+
*
|
|
9
|
+
* Pipeline: AST → [Validator] → Validated AST + Diagnostics
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ─── Diagnostic ────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export class Diagnostic {
|
|
15
|
+
/**
|
|
16
|
+
* @param {'ERROR'|'WARNING'} severity
|
|
17
|
+
* @param {string} message
|
|
18
|
+
* @param {number} [line]
|
|
19
|
+
* @param {number} [column]
|
|
20
|
+
*/
|
|
21
|
+
constructor(severity, message, line = 0, column = 0) {
|
|
22
|
+
this.severity = severity;
|
|
23
|
+
this.message = message;
|
|
24
|
+
this.line = line;
|
|
25
|
+
this.column = column;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
toString() {
|
|
29
|
+
return `[${this.severity}] ${this.line}:${this.column} — ${this.message}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Validation Result ─────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export class ValidationResult {
|
|
36
|
+
constructor() {
|
|
37
|
+
/** @type {Diagnostic[]} */
|
|
38
|
+
this.diagnostics = [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
error(message, line, column) {
|
|
42
|
+
this.diagnostics.push(new Diagnostic('ERROR', message, line, column));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
warning(message, line, column) {
|
|
46
|
+
this.diagnostics.push(new Diagnostic('WARNING', message, line, column));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get errors() {
|
|
50
|
+
return this.diagnostics.filter(d => d.severity === 'ERROR');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get warnings() {
|
|
54
|
+
return this.diagnostics.filter(d => d.severity === 'WARNING');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get isValid() {
|
|
58
|
+
return this.errors.length === 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Semantic Validator ────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const RESERVED_KEYWORDS = new Set([
|
|
65
|
+
'start', 'end', 'workflow', 'agent', 'state', 'flow', 'config',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
export const SUPPORTED_STATE_OPTIONS = {
|
|
69
|
+
required: {
|
|
70
|
+
description: 'Marks the variable as required before workflow execution begins',
|
|
71
|
+
minArgs: 0,
|
|
72
|
+
maxArgs: 0,
|
|
73
|
+
},
|
|
74
|
+
default: {
|
|
75
|
+
description: 'Provides a default initial value for the variable if not provided',
|
|
76
|
+
minArgs: 1,
|
|
77
|
+
maxArgs: 1,
|
|
78
|
+
},
|
|
79
|
+
description: {
|
|
80
|
+
description: 'Human-readable description of the state variable',
|
|
81
|
+
minArgs: 1,
|
|
82
|
+
maxArgs: 1,
|
|
83
|
+
},
|
|
84
|
+
desc: {
|
|
85
|
+
description: 'Shorthand for @description',
|
|
86
|
+
minArgs: 1,
|
|
87
|
+
maxArgs: 1,
|
|
88
|
+
},
|
|
89
|
+
reducer: {
|
|
90
|
+
description: 'Specifies the merge strategy when multiple outputs update this variable (e.g. "append", "replace")',
|
|
91
|
+
minArgs: 1,
|
|
92
|
+
maxArgs: 1,
|
|
93
|
+
},
|
|
94
|
+
min: {
|
|
95
|
+
description: 'Minimum numeric value allowed for int or float variables',
|
|
96
|
+
minArgs: 1,
|
|
97
|
+
maxArgs: 1,
|
|
98
|
+
},
|
|
99
|
+
max: {
|
|
100
|
+
description: 'Maximum numeric value allowed for int or float variables',
|
|
101
|
+
minArgs: 1,
|
|
102
|
+
maxArgs: 1,
|
|
103
|
+
},
|
|
104
|
+
anotheroptions: {
|
|
105
|
+
description: 'Demonstration option allowing multiple parameters',
|
|
106
|
+
minArgs: 0,
|
|
107
|
+
maxArgs: Infinity,
|
|
108
|
+
},
|
|
109
|
+
active: {
|
|
110
|
+
description: 'Marks active status',
|
|
111
|
+
minArgs: 1,
|
|
112
|
+
maxArgs: 1,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export class SemanticValidator {
|
|
117
|
+
/**
|
|
118
|
+
* @param {import('../parser/ast.js').Program} ast
|
|
119
|
+
* @param {string} [filename='<input>']
|
|
120
|
+
*/
|
|
121
|
+
constructor(ast, filename = '<input>') {
|
|
122
|
+
this.ast = ast;
|
|
123
|
+
this.filename = filename;
|
|
124
|
+
this.result = new ValidationResult();
|
|
125
|
+
|
|
126
|
+
/** @type {Map<string, import('../parser/ast.js').AgentBlock>} */
|
|
127
|
+
this.agentMap = new Map();
|
|
128
|
+
|
|
129
|
+
/** @type {Map<string, import('../parser/ast.js').StateField>} */
|
|
130
|
+
this.stateMap = new Map();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Run all validation phases and return the result.
|
|
135
|
+
* @returns {ValidationResult}
|
|
136
|
+
*/
|
|
137
|
+
validate() {
|
|
138
|
+
const workflow = this.ast.workflow;
|
|
139
|
+
|
|
140
|
+
this.symbolResolution(workflow);
|
|
141
|
+
|
|
142
|
+
// Only proceed to later phases if Phase 1 found no fatal structural issues
|
|
143
|
+
if (this.result.isValid) {
|
|
144
|
+
this.referenceValidation(workflow);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.result.isValid && workflow.flow) {
|
|
148
|
+
this.graphValidation(workflow);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return this.result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Phase 1: Symbol Resolution ──────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
symbolResolution(workflow) {
|
|
157
|
+
// 3.1 Workflow name
|
|
158
|
+
if (!workflow.name || workflow.name.trim() === '') {
|
|
159
|
+
this.result.error('Workflow name must be a non-empty string', workflow.line, workflow.column);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 3.5 Block cardinality — flow is required
|
|
163
|
+
if (!workflow.flow) {
|
|
164
|
+
this.result.error('Missing flow block', workflow.line, workflow.column);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 3.5 At least one agent
|
|
168
|
+
if (workflow.agents.length === 0) {
|
|
169
|
+
this.result.error('No agents declared', workflow.line, workflow.column);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 3.2 Unique agent identifiers + 3.4 Reserved keywords
|
|
173
|
+
for (const agent of workflow.agents) {
|
|
174
|
+
if (RESERVED_KEYWORDS.has(agent.id)) {
|
|
175
|
+
this.result.error(
|
|
176
|
+
`Reserved keyword used as agent identifier: "${agent.id}"`,
|
|
177
|
+
agent.line, agent.column
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.agentMap.has(agent.id)) {
|
|
182
|
+
this.result.error(
|
|
183
|
+
`Duplicate agent identifier: "${agent.id}"`,
|
|
184
|
+
agent.line, agent.column
|
|
185
|
+
);
|
|
186
|
+
} else {
|
|
187
|
+
this.agentMap.set(agent.id, agent);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 3.3 Unique state variables and options validation
|
|
192
|
+
if (workflow.state) {
|
|
193
|
+
for (const field of workflow.state.fields) {
|
|
194
|
+
if (this.stateMap.has(field.name)) {
|
|
195
|
+
this.result.error(
|
|
196
|
+
`Duplicate state variable: "${field.name}"`,
|
|
197
|
+
field.line, field.column
|
|
198
|
+
);
|
|
199
|
+
} else {
|
|
200
|
+
this.stateMap.set(field.name, field);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (field.options && field.options.length > 0) {
|
|
204
|
+
const seenOptions = new Set();
|
|
205
|
+
for (const opt of field.options) {
|
|
206
|
+
if (seenOptions.has(opt.name)) {
|
|
207
|
+
this.result.error(
|
|
208
|
+
`Duplicate option "@${opt.name}" on state variable "${field.name}"`,
|
|
209
|
+
opt.line, opt.column
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
seenOptions.add(opt.name);
|
|
213
|
+
|
|
214
|
+
const spec = SUPPORTED_STATE_OPTIONS[opt.name];
|
|
215
|
+
if (!spec) {
|
|
216
|
+
const supportedList = Object.keys(SUPPORTED_STATE_OPTIONS).map(k => `@${k}`).join(', ');
|
|
217
|
+
this.result.error(
|
|
218
|
+
`Unsupported option "@${opt.name}" on state variable "${field.name}". Supported options are: ${supportedList}`,
|
|
219
|
+
opt.line, opt.column
|
|
220
|
+
);
|
|
221
|
+
} else {
|
|
222
|
+
const argCount = opt.args ? opt.args.length : 0;
|
|
223
|
+
if (argCount < spec.minArgs || argCount > spec.maxArgs) {
|
|
224
|
+
if (spec.minArgs === 0 && spec.maxArgs === 0) {
|
|
225
|
+
this.result.error(
|
|
226
|
+
`Option "@${opt.name}" does not take arguments`,
|
|
227
|
+
opt.line, opt.column
|
|
228
|
+
);
|
|
229
|
+
} else if (spec.minArgs === spec.maxArgs) {
|
|
230
|
+
this.result.error(
|
|
231
|
+
`Option "@${opt.name}" expects exactly ${spec.minArgs} argument(s), but found ${argCount}`,
|
|
232
|
+
opt.line, opt.column
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
this.result.error(
|
|
236
|
+
`Option "@${opt.name}" expects between ${spec.minArgs} and ${spec.maxArgs} argument(s), but found ${argCount}`,
|
|
237
|
+
opt.line, opt.column
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Phase 2: Reference Validation ───────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
referenceValidation(workflow) {
|
|
251
|
+
// 4.1 Flow references
|
|
252
|
+
if (workflow.flow) {
|
|
253
|
+
for (const edge of workflow.flow.edges) {
|
|
254
|
+
if (edge.source === 'end') {
|
|
255
|
+
this.result.error(`Outgoing edge from end node not allowed: end -> ${edge.target}`, edge.line, edge.column);
|
|
256
|
+
} else if (edge.source !== 'start' && !this.agentMap.has(edge.source)) {
|
|
257
|
+
this.result.error(
|
|
258
|
+
`Undefined agent in flow: "${edge.source}"`,
|
|
259
|
+
edge.line, edge.column
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (edge.target === 'start') {
|
|
263
|
+
this.result.error(`Incoming edge to start node not allowed: ${edge.source} -> start`, edge.line, edge.column);
|
|
264
|
+
} else if (edge.target !== 'end' && !this.agentMap.has(edge.target)) {
|
|
265
|
+
this.result.error(
|
|
266
|
+
`Undefined agent in flow: "${edge.target}"`,
|
|
267
|
+
edge.line, edge.column
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 4.2 Input/output references
|
|
274
|
+
const allOutputs = new Set();
|
|
275
|
+
const requiredOrDefaultVars = new Set();
|
|
276
|
+
if (workflow.state) {
|
|
277
|
+
for (const field of workflow.state.fields) {
|
|
278
|
+
for (const opt of field.options) {
|
|
279
|
+
if (opt.name === 'required' || opt.name === 'default') {
|
|
280
|
+
requiredOrDefaultVars.add(field.name);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const agent of workflow.agents) {
|
|
286
|
+
for (const outputVar of agent.outputs) {
|
|
287
|
+
allOutputs.add(outputVar);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const agent of workflow.agents) {
|
|
292
|
+
const seenInputs = new Set();
|
|
293
|
+
for (const inputVar of agent.inputs) {
|
|
294
|
+
if (seenInputs.has(inputVar)) {
|
|
295
|
+
this.result.error(
|
|
296
|
+
`Duplicate input variable "${inputVar}" in agent "${agent.id}"`,
|
|
297
|
+
agent.line, agent.column
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
seenInputs.add(inputVar);
|
|
301
|
+
|
|
302
|
+
if (!this.stateMap.has(inputVar)) {
|
|
303
|
+
this.result.error(
|
|
304
|
+
`Undefined state variable in agent "${agent.id}": "${inputVar}"`,
|
|
305
|
+
agent.line, agent.column
|
|
306
|
+
);
|
|
307
|
+
} else if (!allOutputs.has(inputVar) && !requiredOrDefaultVars.has(inputVar)) {
|
|
308
|
+
this.result.warning(
|
|
309
|
+
`State variable "${inputVar}" is read by agent "${agent.id}" but is never initialized or output by any agent`,
|
|
310
|
+
agent.line, agent.column
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const seenOutputs = new Set();
|
|
315
|
+
for (const outputVar of agent.outputs) {
|
|
316
|
+
if (seenOutputs.has(outputVar)) {
|
|
317
|
+
this.result.error(
|
|
318
|
+
`Duplicate output variable "${outputVar}" in agent "${agent.id}"`,
|
|
319
|
+
agent.line, agent.column
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
seenOutputs.add(outputVar);
|
|
323
|
+
|
|
324
|
+
if (!this.stateMap.has(outputVar)) {
|
|
325
|
+
this.result.error(
|
|
326
|
+
`Undefined state variable in agent "${agent.id}": "${outputVar}"`,
|
|
327
|
+
agent.line, agent.column
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 4.3 Temperature range
|
|
334
|
+
for (const agent of workflow.agents) {
|
|
335
|
+
if (agent.temperature !== null) {
|
|
336
|
+
if (agent.temperature < 0.0 || agent.temperature > 2.0) {
|
|
337
|
+
this.result.error(
|
|
338
|
+
`Invalid temperature value for agent "${agent.id}": ${agent.temperature} (must be 0.0–2.0)`,
|
|
339
|
+
agent.line, agent.column
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 4.4 Provider & Model validation
|
|
346
|
+
for (const agent of workflow.agents) {
|
|
347
|
+
if (agent.provider !== null) {
|
|
348
|
+
if (agent.provider !== 'gemini' && agent.provider !== 'openai' && agent.provider !== 'anthropic') {
|
|
349
|
+
this.result.error(
|
|
350
|
+
`Invalid provider value for agent "${agent.id}": "${agent.provider}" (must be "gemini", "openai", or "anthropic")`,
|
|
351
|
+
agent.line, agent.column
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (agent.model !== null && agent.model.trim() === '') {
|
|
356
|
+
this.result.error(
|
|
357
|
+
`Agent "${agent.id}" model cannot be empty string`,
|
|
358
|
+
agent.line, agent.column
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 4.5 Config entries validation
|
|
364
|
+
if (workflow.config) {
|
|
365
|
+
for (const entry of workflow.config.entries) {
|
|
366
|
+
if (entry.key === 'max_iterations') {
|
|
367
|
+
if (typeof entry.value !== 'number' || !Number.isInteger(entry.value) || entry.value <= 0) {
|
|
368
|
+
this.result.error(
|
|
369
|
+
`Configuration "max_iterations" must be a positive integer, found ${JSON.stringify(entry.value)}`,
|
|
370
|
+
entry.line, entry.column
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
} else if (entry.key === 'timeout_seconds') {
|
|
374
|
+
if (typeof entry.value !== 'number' || entry.value <= 0) {
|
|
375
|
+
this.result.error(
|
|
376
|
+
`Configuration "timeout_seconds" must be a positive number, found ${JSON.stringify(entry.value)}`,
|
|
377
|
+
entry.line, entry.column
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
} else if (entry.key === 'runtime') {
|
|
381
|
+
if (entry.value !== 'langgraph') {
|
|
382
|
+
this.result.error(
|
|
383
|
+
`Unsupported runtime "${entry.value}" in configuration (supported: "langgraph")`,
|
|
384
|
+
entry.line, entry.column
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Warnings: unused state variables
|
|
392
|
+
if (workflow.state) {
|
|
393
|
+
const referencedVars = new Set();
|
|
394
|
+
for (const agent of workflow.agents) {
|
|
395
|
+
agent.inputs.forEach(v => referencedVars.add(v));
|
|
396
|
+
agent.outputs.forEach(v => referencedVars.add(v));
|
|
397
|
+
}
|
|
398
|
+
for (const field of workflow.state.fields) {
|
|
399
|
+
if (!referencedVars.has(field.name)) {
|
|
400
|
+
this.result.warning(
|
|
401
|
+
`State variable "${field.name}" is never referenced`,
|
|
402
|
+
field.line, field.column
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Warnings: agents without inputs/outputs
|
|
409
|
+
for (const agent of workflow.agents) {
|
|
410
|
+
if (agent.inputs.length === 0 && agent.outputs.length === 0) {
|
|
411
|
+
this.result.warning(
|
|
412
|
+
`Agent "${agent.id}" does not declare inputs or outputs`,
|
|
413
|
+
agent.line, agent.column
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ── Phase 3: Graph Validation ───────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
graphValidation(workflow) {
|
|
422
|
+
const edges = workflow.flow.edges;
|
|
423
|
+
|
|
424
|
+
for (const edge of edges) {
|
|
425
|
+
if (edge.target === 'start') {
|
|
426
|
+
this.result.error(`Incoming edge to start node not allowed: ${edge.source} -> start`, edge.line, edge.column);
|
|
427
|
+
}
|
|
428
|
+
if (edge.source === 'end') {
|
|
429
|
+
this.result.error(`Outgoing edge from end node not allowed: end -> ${edge.target}`, edge.line, edge.column);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 5.1 Start node: exactly one edge from start
|
|
434
|
+
const startEdges = edges.filter(e => e.source === 'start');
|
|
435
|
+
if (startEdges.length === 0) {
|
|
436
|
+
this.result.error('Missing start edge', workflow.flow.line, workflow.flow.column);
|
|
437
|
+
} else if (startEdges.length > 1) {
|
|
438
|
+
this.result.error('Multiple start edges', startEdges[1].line, startEdges[1].column);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 5.2 End node: at least one edge to end
|
|
442
|
+
const endEdges = edges.filter(e => e.target === 'end');
|
|
443
|
+
if (endEdges.length === 0) {
|
|
444
|
+
this.result.error('No edge leads to end', workflow.flow.line, workflow.flow.column);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 5.5 Duplicate edges
|
|
448
|
+
const edgeSet = new Set();
|
|
449
|
+
for (const edge of edges) {
|
|
450
|
+
const key = `${edge.source}->${edge.target}`;
|
|
451
|
+
if (edgeSet.has(key)) {
|
|
452
|
+
this.result.error(
|
|
453
|
+
`Duplicate edge: ${edge.source} -> ${edge.target}`,
|
|
454
|
+
edge.line, edge.column
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
edgeSet.add(key);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 5.6 Self-loops
|
|
461
|
+
for (const edge of edges) {
|
|
462
|
+
if (edge.source === edge.target) {
|
|
463
|
+
this.result.error(
|
|
464
|
+
`Self-loop detected: ${edge.source} -> ${edge.target}`,
|
|
465
|
+
edge.line, edge.column
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Build adjacency list (agent-to-agent, including start/end)
|
|
471
|
+
const adj = new Map();
|
|
472
|
+
const revAdj = new Map();
|
|
473
|
+
const allNodes = new Set(['start', 'end']);
|
|
474
|
+
for (const agent of workflow.agents) {
|
|
475
|
+
allNodes.add(agent.id);
|
|
476
|
+
adj.set(agent.id, []);
|
|
477
|
+
revAdj.set(agent.id, []);
|
|
478
|
+
}
|
|
479
|
+
adj.set('start', []);
|
|
480
|
+
adj.set('end', []);
|
|
481
|
+
revAdj.set('start', []);
|
|
482
|
+
revAdj.set('end', []);
|
|
483
|
+
|
|
484
|
+
for (const edge of edges) {
|
|
485
|
+
if (adj.has(edge.source) && allNodes.has(edge.target)) {
|
|
486
|
+
adj.get(edge.source).push(edge.target);
|
|
487
|
+
}
|
|
488
|
+
if (revAdj.has(edge.target) && allNodes.has(edge.source)) {
|
|
489
|
+
revAdj.get(edge.target).push(edge.source);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 5.3 Reachability from start
|
|
494
|
+
const reachableFromStart = this.bfs(adj, 'start');
|
|
495
|
+
for (const agent of workflow.agents) {
|
|
496
|
+
if (!reachableFromStart.has(agent.id)) {
|
|
497
|
+
this.result.error(
|
|
498
|
+
`Unreachable agent: "${agent.id}"`,
|
|
499
|
+
agent.line, agent.column
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 5.4 Termination: all agents have path to end
|
|
505
|
+
const reachableFromEnd = this.bfs(revAdj, 'end');
|
|
506
|
+
for (const agent of workflow.agents) {
|
|
507
|
+
if (!reachableFromEnd.has(agent.id)) {
|
|
508
|
+
this.result.error(
|
|
509
|
+
`Agent has no path to end: "${agent.id}"`,
|
|
510
|
+
agent.line, agent.column
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 5.7 Acyclicity (DFS-based cycle detection among agent nodes)
|
|
516
|
+
this.detectCycles(workflow);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* BFS from a starting node, returning the set of reachable nodes.
|
|
521
|
+
*/
|
|
522
|
+
bfs(adj, start) {
|
|
523
|
+
const visited = new Set();
|
|
524
|
+
const queue = [start];
|
|
525
|
+
visited.add(start);
|
|
526
|
+
|
|
527
|
+
while (queue.length > 0) {
|
|
528
|
+
const node = queue.shift();
|
|
529
|
+
const neighbors = adj.get(node) ?? [];
|
|
530
|
+
for (const neighbor of neighbors) {
|
|
531
|
+
if (!visited.has(neighbor)) {
|
|
532
|
+
visited.add(neighbor);
|
|
533
|
+
queue.push(neighbor);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return visited;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Detect cycles using DFS coloring (white/gray/black).
|
|
543
|
+
*/
|
|
544
|
+
detectCycles(workflow) {
|
|
545
|
+
const agentIds = workflow.agents.map(a => a.id);
|
|
546
|
+
|
|
547
|
+
// Build agent-only adjacency (exclude start/end)
|
|
548
|
+
const adj = new Map();
|
|
549
|
+
for (const id of agentIds) {
|
|
550
|
+
adj.set(id, []);
|
|
551
|
+
}
|
|
552
|
+
for (const edge of workflow.flow.edges) {
|
|
553
|
+
if (edge.source !== 'start' && edge.target !== 'end') {
|
|
554
|
+
if (adj.has(edge.source)) {
|
|
555
|
+
adj.get(edge.source).push(edge.target);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
561
|
+
const color = new Map();
|
|
562
|
+
for (const id of agentIds) color.set(id, WHITE);
|
|
563
|
+
|
|
564
|
+
const cycleNodes = [];
|
|
565
|
+
|
|
566
|
+
const dfs = (node) => {
|
|
567
|
+
color.set(node, GRAY);
|
|
568
|
+
for (const neighbor of (adj.get(node) ?? [])) {
|
|
569
|
+
if (color.get(neighbor) === GRAY) {
|
|
570
|
+
cycleNodes.push(neighbor);
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
if (color.get(neighbor) === WHITE && dfs(neighbor)) {
|
|
574
|
+
cycleNodes.push(neighbor);
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
color.set(node, BLACK);
|
|
579
|
+
return false;
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
for (const id of agentIds) {
|
|
583
|
+
if (color.get(id) === WHITE) {
|
|
584
|
+
if (dfs(id)) {
|
|
585
|
+
this.result.error(
|
|
586
|
+
`Cycle detected in flow graph involving: ${[...new Set(cycleNodes)].join(', ')}`,
|
|
587
|
+
workflow.flow.line, workflow.flow.column
|
|
588
|
+
);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Three-agent pipeline: research → design → implement
|
|
2
|
+
workflow "Software Development" {
|
|
3
|
+
|
|
4
|
+
state {
|
|
5
|
+
requirements: string
|
|
6
|
+
analysis: string
|
|
7
|
+
architecture: string
|
|
8
|
+
implementation: string
|
|
9
|
+
status: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
agent Analyst {
|
|
13
|
+
instructions: """
|
|
14
|
+
Analyze the requirements document.
|
|
15
|
+
Identify key features, constraints, and acceptance criteria.
|
|
16
|
+
Produce a structured analysis.
|
|
17
|
+
"""
|
|
18
|
+
model: "gemma-4-26b-a4b-it"
|
|
19
|
+
temperature: 0.3
|
|
20
|
+
inputs: [requirements]
|
|
21
|
+
outputs: [analysis]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
agent Architect {
|
|
25
|
+
instructions: """
|
|
26
|
+
Based on the analysis, design a technical architecture.
|
|
27
|
+
Include component diagrams, data flow, and API contracts.
|
|
28
|
+
"""
|
|
29
|
+
model: "gemma-4-26b-a4b-it"
|
|
30
|
+
|
|
31
|
+
temperature: 0.5
|
|
32
|
+
inputs: [analysis]
|
|
33
|
+
outputs: [architecture]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
agent Developer {
|
|
37
|
+
instructions: """
|
|
38
|
+
Implement the solution based on the architecture.
|
|
39
|
+
Write clean, tested, production-ready code.
|
|
40
|
+
"""
|
|
41
|
+
model: "gemma-4-26b-a4b-it"
|
|
42
|
+
temperature: 0.2
|
|
43
|
+
tools: ["code_interpreter", "file_writer"]
|
|
44
|
+
inputs: [architecture]
|
|
45
|
+
outputs: [implementation, status]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
flow {
|
|
49
|
+
start -> Analyst
|
|
50
|
+
Analyst -> Architect
|
|
51
|
+
Architect -> Developer
|
|
52
|
+
Developer -> end
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
config {
|
|
56
|
+
version: "0.1"
|
|
57
|
+
timeout_seconds: 600
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source_text": "The landscape of artificial intelligence is rapidly shifting from single, monolithic large language models to complex, multi-agent systems. Rather than relying on one generalized AI to handle an entire task from start to finish, developers are increasingly building collaborative environments where specialized agents handle distinct parts of a pipeline. For example, one agent might be responsible for gathering and parsing external data, while another focuses exclusively on executing code or validating the output against strict constraints. This modular approach not only improves the reliability of the final output but also allows for smaller, highly optimized models to outperform larger ones on specific sub-tasks.\n\nHowever, orchestrating these multi-agent workflows introduces significant architectural challenges. As agents interact, system designers must maintain a robust state management layer to track the context and history of the process, ensuring that no critical information is lost between handoffs. Furthermore, defining clear communication protocols is essential to prevent infinite loops and ensure that agents trigger each other in the correct sequence. Error handling also becomes exponentially more complex, requiring fallback mechanisms that can gracefully recover when a single agent fails to complete its designated role or produces a hallucination.\n\nTo address these bottlenecks, the developer community is moving toward standardized, open-source frameworks designed to specify and execute multi-agent workflows efficiently. By providing a unified syntax for agent definitions, prompt caching strategies, and environment configurations, these frameworks lower the barrier to entry for engineering complex AI applications. As these tools mature, we can expect a future where deploying a resilient, autonomous team of AI agents is as straightforward as containerizing a traditional web application, fundamentally changing how software is built and maintained."
|
|
3
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
workflow "Quick Summarize" {
|
|
2
|
+
|
|
3
|
+
state {
|
|
4
|
+
source_text: string @required
|
|
5
|
+
extracted_points: string
|
|
6
|
+
summary: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
agent Extractor {
|
|
10
|
+
instructions: "Read the `source_text` and extract the most important facts into a concise bulleted list."
|
|
11
|
+
model: "gemini-2.0-flash"
|
|
12
|
+
inputs: [source_text]
|
|
13
|
+
outputs: [extracted_points]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
agent Synthesizer {
|
|
17
|
+
instructions: "Take the extracted points and weave them into a clear, cohesive summary paragraph."
|
|
18
|
+
model: "gpt-4o"
|
|
19
|
+
inputs: [extracted_points]
|
|
20
|
+
outputs: [summary]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
flow {
|
|
24
|
+
start -> Extractor
|
|
25
|
+
Extractor -> Synthesizer
|
|
26
|
+
Synthesizer -> end
|
|
27
|
+
}
|
|
28
|
+
}
|