agentopology 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 +191 -0
- package/README.md +450 -0
- package/dist/analyzer/__tests__/analyzer.test.d.ts +2 -0
- package/dist/analyzer/__tests__/analyzer.test.d.ts.map +1 -0
- package/dist/analyzer/__tests__/analyzer.test.js +258 -0
- package/dist/analyzer/__tests__/analyzer.test.js.map +1 -0
- package/dist/analyzer/index.d.ts +44 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +308 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/bindings/__tests__/bindings.test.d.ts +2 -0
- package/dist/bindings/__tests__/bindings.test.d.ts.map +1 -0
- package/dist/bindings/__tests__/bindings.test.js +2089 -0
- package/dist/bindings/__tests__/bindings.test.js.map +1 -0
- package/dist/bindings/claude-code.d.ts +12 -0
- package/dist/bindings/claude-code.d.ts.map +1 -0
- package/dist/bindings/claude-code.js +2063 -0
- package/dist/bindings/claude-code.js.map +1 -0
- package/dist/bindings/codex.d.ts +18 -0
- package/dist/bindings/codex.d.ts.map +1 -0
- package/dist/bindings/codex.js +1633 -0
- package/dist/bindings/codex.js.map +1 -0
- package/dist/bindings/copilot-cli.d.ts +20 -0
- package/dist/bindings/copilot-cli.d.ts.map +1 -0
- package/dist/bindings/copilot-cli.js +1515 -0
- package/dist/bindings/copilot-cli.js.map +1 -0
- package/dist/bindings/cursor.d.ts +17 -0
- package/dist/bindings/cursor.d.ts.map +1 -0
- package/dist/bindings/cursor.js +1715 -0
- package/dist/bindings/cursor.js.map +1 -0
- package/dist/bindings/gemini-cli.d.ts +18 -0
- package/dist/bindings/gemini-cli.d.ts.map +1 -0
- package/dist/bindings/gemini-cli.js +1327 -0
- package/dist/bindings/gemini-cli.js.map +1 -0
- package/dist/bindings/index.d.ts +20 -0
- package/dist/bindings/index.d.ts.map +1 -0
- package/dist/bindings/index.js +33 -0
- package/dist/bindings/index.js.map +1 -0
- package/dist/bindings/kiro.d.ts +17 -0
- package/dist/bindings/kiro.d.ts.map +1 -0
- package/dist/bindings/kiro.js +1624 -0
- package/dist/bindings/kiro.js.map +1 -0
- package/dist/bindings/openclaw.d.ts +18 -0
- package/dist/bindings/openclaw.d.ts.map +1 -0
- package/dist/bindings/openclaw.js +1578 -0
- package/dist/bindings/openclaw.js.map +1 -0
- package/dist/bindings/types.d.ts +33 -0
- package/dist/bindings/types.d.ts.map +1 -0
- package/dist/bindings/types.js +23 -0
- package/dist/bindings/types.js.map +1 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +679 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/docs/__tests__/docs.test.d.ts +2 -0
- package/dist/docs/__tests__/docs.test.d.ts.map +1 -0
- package/dist/docs/__tests__/docs.test.js +438 -0
- package/dist/docs/__tests__/docs.test.js.map +1 -0
- package/dist/docs/content.d.ts +16 -0
- package/dist/docs/content.d.ts.map +1 -0
- package/dist/docs/content.js +3273 -0
- package/dist/docs/content.js.map +1 -0
- package/dist/docs/index.d.ts +17 -0
- package/dist/docs/index.d.ts.map +1 -0
- package/dist/docs/index.js +117 -0
- package/dist/docs/index.js.map +1 -0
- package/dist/exporters/__tests__/exporters.test.d.ts +2 -0
- package/dist/exporters/__tests__/exporters.test.d.ts.map +1 -0
- package/dist/exporters/__tests__/exporters.test.js +731 -0
- package/dist/exporters/__tests__/exporters.test.js.map +1 -0
- package/dist/exporters/index.d.ts +13 -0
- package/dist/exporters/index.d.ts.map +1 -0
- package/dist/exporters/index.js +18 -0
- package/dist/exporters/index.js.map +1 -0
- package/dist/exporters/json.d.ts +8 -0
- package/dist/exporters/json.d.ts.map +1 -0
- package/dist/exporters/json.js +25 -0
- package/dist/exporters/json.js.map +1 -0
- package/dist/exporters/markdown.d.ts +9 -0
- package/dist/exporters/markdown.d.ts.map +1 -0
- package/dist/exporters/markdown.js +686 -0
- package/dist/exporters/markdown.js.map +1 -0
- package/dist/exporters/mermaid.d.ts +11 -0
- package/dist/exporters/mermaid.d.ts.map +1 -0
- package/dist/exporters/mermaid.js +161 -0
- package/dist/exporters/mermaid.js.map +1 -0
- package/dist/exporters/types.d.ts +22 -0
- package/dist/exporters/types.d.ts.map +1 -0
- package/dist/exporters/types.js +10 -0
- package/dist/exporters/types.js.map +1 -0
- package/dist/import/__tests__/import.test.d.ts +2 -0
- package/dist/import/__tests__/import.test.d.ts.map +1 -0
- package/dist/import/__tests__/import.test.js +781 -0
- package/dist/import/__tests__/import.test.js.map +1 -0
- package/dist/import/claude-code.d.ts +38 -0
- package/dist/import/claude-code.d.ts.map +1 -0
- package/dist/import/claude-code.js +897 -0
- package/dist/import/claude-code.js.map +1 -0
- package/dist/import/index.d.ts +18 -0
- package/dist/import/index.d.ts.map +1 -0
- package/dist/import/index.js +28 -0
- package/dist/import/index.js.map +1 -0
- package/dist/import/serializer.d.ts +11 -0
- package/dist/import/serializer.d.ts.map +1 -0
- package/dist/import/serializer.js +960 -0
- package/dist/import/serializer.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/__tests__/parser.test.d.ts +2 -0
- package/dist/parser/__tests__/parser.test.d.ts.map +1 -0
- package/dist/parser/__tests__/parser.test.js +6514 -0
- package/dist/parser/__tests__/parser.test.js.map +1 -0
- package/dist/parser/ast.d.ts +708 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +12 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/index.d.ts +253 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +1985 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/lexer.d.ts +100 -0
- package/dist/parser/lexer.d.ts.map +1 -0
- package/dist/parser/lexer.js +304 -0
- package/dist/parser/lexer.js.map +1 -0
- package/dist/parser/validator.d.ts +42 -0
- package/dist/parser/validator.d.ts.map +1 -0
- package/dist/parser/validator.js +2568 -0
- package/dist/parser/validator.js.map +1 -0
- package/dist/scaffold/__tests__/scaffold.test.d.ts +7 -0
- package/dist/scaffold/__tests__/scaffold.test.d.ts.map +1 -0
- package/dist/scaffold/__tests__/scaffold.test.js +295 -0
- package/dist/scaffold/__tests__/scaffold.test.js.map +1 -0
- package/dist/scaffold/incremental.d.ts +43 -0
- package/dist/scaffold/incremental.d.ts.map +1 -0
- package/dist/scaffold/incremental.js +185 -0
- package/dist/scaffold/incremental.js.map +1 -0
- package/dist/scaffold/index.d.ts +13 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +13 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/manifest.d.ts +30 -0
- package/dist/scaffold/manifest.d.ts.map +1 -0
- package/dist/scaffold/manifest.js +71 -0
- package/dist/scaffold/manifest.js.map +1 -0
- package/dist/scaffold/merge.d.ts +52 -0
- package/dist/scaffold/merge.d.ts.map +1 -0
- package/dist/scaffold/merge.js +134 -0
- package/dist/scaffold/merge.js.map +1 -0
- package/dist/scaffold/types.d.ts +51 -0
- package/dist/scaffold/types.d.ts.map +1 -0
- package/dist/scaffold/types.js +10 -0
- package/dist/scaffold/types.js.map +1 -0
- package/dist/sync/index.d.ts +34 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +392 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/visualizer/index.d.ts +17 -0
- package/dist/visualizer/index.d.ts.map +1 -0
- package/dist/visualizer/index.js +1976 -0
- package/dist/visualizer/index.js.map +1 -0
- package/examples/code-review.at +167 -0
- package/examples/data-processing.at +296 -0
- package/examples/openclaw-assistant.at +142 -0
- package/examples/scheduled-monitor.at +193 -0
- package/examples/simple-pipeline.at +163 -0
- package/package.json +77 -0
- package/skill/SKILL.md +403 -0
- package/skill/scripts/emit.ts +853 -0
- package/skill/scripts/visualize.ts +89 -0
- package/spec/grammar.md +1466 -0
- package/spec/reserved-keywords.md +147 -0
- package/spec/validation.md +535 -0
|
@@ -0,0 +1,1985 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgenTopology parser.
|
|
3
|
+
*
|
|
4
|
+
* Takes the raw text of an `.at` file and produces a fully-typed
|
|
5
|
+
* {@link TopologyAST}. Individual section parsers are also exported for
|
|
6
|
+
* advanced / incremental use.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { parse } from "./parser";
|
|
10
|
+
* const ast = parse(source);
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { stripComments, extractBlock, extractAllBlocks, parseKV, parseList, parseMultilineList, unquote, parseFields, parseOutputsBlock, dedentBlock, } from "./lexer.js";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/** Convert a kebab-case or snake_case identifier to a Title Case label. */
|
|
20
|
+
function toLabel(id) {
|
|
21
|
+
return id
|
|
22
|
+
.split(/[-_]/)
|
|
23
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
24
|
+
.join(" ");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse an `extensions { binding-name { key: value } }` sub-block.
|
|
28
|
+
*
|
|
29
|
+
* Each named sub-block inside `extensions` becomes a key in the returned
|
|
30
|
+
* record, with its key-value pairs parsed via {@link parseFields}.
|
|
31
|
+
*/
|
|
32
|
+
function parseExtensionsBlock(block) {
|
|
33
|
+
const extBlock = extractBlock(block, "extensions");
|
|
34
|
+
if (!extBlock)
|
|
35
|
+
return undefined;
|
|
36
|
+
const result = {};
|
|
37
|
+
// Extract each named sub-block inside extensions
|
|
38
|
+
const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
|
|
39
|
+
let m;
|
|
40
|
+
while ((m = re.exec(extBlock.body)) !== null) {
|
|
41
|
+
const name = m[1];
|
|
42
|
+
const startIdx = m.index + m[0].length;
|
|
43
|
+
let depth = 1;
|
|
44
|
+
let i = startIdx;
|
|
45
|
+
while (i < extBlock.body.length && depth > 0) {
|
|
46
|
+
if (extBlock.body[i] === "{")
|
|
47
|
+
depth++;
|
|
48
|
+
else if (extBlock.body[i] === "}")
|
|
49
|
+
depth--;
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
if (depth === 0) {
|
|
53
|
+
const innerBody = extBlock.body.slice(startIdx, i - 1);
|
|
54
|
+
const fields = parseFields(innerBody);
|
|
55
|
+
const entry = {};
|
|
56
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
57
|
+
// Try to parse booleans and numbers
|
|
58
|
+
if (v === "true")
|
|
59
|
+
entry[k] = true;
|
|
60
|
+
else if (v === "false")
|
|
61
|
+
entry[k] = false;
|
|
62
|
+
else if (/^\d+$/.test(v))
|
|
63
|
+
entry[k] = parseInt(v, 10);
|
|
64
|
+
else
|
|
65
|
+
entry[k] = unquote(v);
|
|
66
|
+
}
|
|
67
|
+
result[name] = entry;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Schema parsing
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
/** Known primitive type names for the schema type system. */
|
|
76
|
+
const SCHEMA_PRIMITIVES = new Set(["string", "number", "integer", "boolean", "object"]);
|
|
77
|
+
/**
|
|
78
|
+
* Parse a type expression string into a {@link SchemaType}.
|
|
79
|
+
*
|
|
80
|
+
* Supports:
|
|
81
|
+
* - Primitives: `"string"`, `"number"`, `"integer"`, `"boolean"`, `"object"`
|
|
82
|
+
* - Arrays: `"array of string"`, `"array of finding"`
|
|
83
|
+
* - Enums: `"low | medium | high"`
|
|
84
|
+
* - References: any non-primitive identifier like `"finding"`
|
|
85
|
+
*/
|
|
86
|
+
export function parseSchemaType(typeStr) {
|
|
87
|
+
const trimmed = typeStr.trim();
|
|
88
|
+
// Enum: contains ` | ` separator
|
|
89
|
+
if (trimmed.includes("|")) {
|
|
90
|
+
const values = trimmed.split("|").map((v) => v.trim()).filter(Boolean);
|
|
91
|
+
return { kind: "enum", values };
|
|
92
|
+
}
|
|
93
|
+
// Array: "array of <type>"
|
|
94
|
+
const arrayMatch = trimmed.match(/^array\s+of\s+(.+)$/i);
|
|
95
|
+
if (arrayMatch) {
|
|
96
|
+
return { kind: "array", itemType: parseSchemaType(arrayMatch[1]) };
|
|
97
|
+
}
|
|
98
|
+
// Primitive
|
|
99
|
+
if (SCHEMA_PRIMITIVES.has(trimmed)) {
|
|
100
|
+
return { kind: "primitive", value: trimmed };
|
|
101
|
+
}
|
|
102
|
+
// Reference to a named schema
|
|
103
|
+
return { kind: "ref", name: trimmed };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parse a block body of `name: type-expression` lines into schema field definitions.
|
|
107
|
+
*
|
|
108
|
+
* Each line is parsed as a KV pair. A leading `?` on the type expression marks
|
|
109
|
+
* the field as optional.
|
|
110
|
+
*/
|
|
111
|
+
export function parseSchemaFields(body) {
|
|
112
|
+
const fields = [];
|
|
113
|
+
for (const line of body.split("\n")) {
|
|
114
|
+
const kv = parseKV(line);
|
|
115
|
+
if (!kv)
|
|
116
|
+
continue;
|
|
117
|
+
const [name, rawType] = kv;
|
|
118
|
+
let typeStr = rawType.trim();
|
|
119
|
+
let optional = false;
|
|
120
|
+
if (typeStr.startsWith("?")) {
|
|
121
|
+
optional = true;
|
|
122
|
+
typeStr = typeStr.slice(1).trim();
|
|
123
|
+
}
|
|
124
|
+
fields.push({
|
|
125
|
+
name,
|
|
126
|
+
type: parseSchemaType(typeStr),
|
|
127
|
+
optional,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return fields;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Parse the top-level `schemas { schema <id> { ... } ... }` block.
|
|
134
|
+
*
|
|
135
|
+
* Returns an array of named schema definitions.
|
|
136
|
+
*/
|
|
137
|
+
export function parseSchemas(topBody) {
|
|
138
|
+
const schemasBlock = extractBlock(topBody, "schemas");
|
|
139
|
+
if (!schemasBlock)
|
|
140
|
+
return [];
|
|
141
|
+
const schemaBlocks = extractAllBlocks(schemasBlock.body, "schema");
|
|
142
|
+
const schemas = [];
|
|
143
|
+
for (const block of schemaBlocks) {
|
|
144
|
+
if (!block.id)
|
|
145
|
+
continue;
|
|
146
|
+
schemas.push({
|
|
147
|
+
id: block.id,
|
|
148
|
+
fields: parseSchemaFields(block.body),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return schemas;
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Section parsers
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
/**
|
|
157
|
+
* Parse the topology header line.
|
|
158
|
+
*
|
|
159
|
+
* Expects `topology <name> : [pattern, ...] {` somewhere in the source.
|
|
160
|
+
*
|
|
161
|
+
* @throws If no topology header is found.
|
|
162
|
+
*/
|
|
163
|
+
export function parseTopologyHeader(src) {
|
|
164
|
+
const m = src.match(/topology\s+(\S+)\s*:\s*\[([^\]]*)\]\s*\{/);
|
|
165
|
+
if (!m)
|
|
166
|
+
throw new Error("Could not find topology header");
|
|
167
|
+
return {
|
|
168
|
+
name: m[1],
|
|
169
|
+
patterns: m[2]
|
|
170
|
+
.split(",")
|
|
171
|
+
.map((s) => s.trim())
|
|
172
|
+
.filter(Boolean),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Parse the `meta { ... }` block.
|
|
177
|
+
*
|
|
178
|
+
* Extracts version, description, foundations, and advanced fields.
|
|
179
|
+
*/
|
|
180
|
+
export function parseMeta(body) {
|
|
181
|
+
const fields = parseFields(body);
|
|
182
|
+
const result = {};
|
|
183
|
+
if (fields.version)
|
|
184
|
+
result.version = unquote(fields.version);
|
|
185
|
+
if (fields.description)
|
|
186
|
+
result.description = unquote(fields.description);
|
|
187
|
+
if (fields.domain)
|
|
188
|
+
result.domain = fields.domain;
|
|
189
|
+
const foundations = parseMultilineList(body, "foundations");
|
|
190
|
+
if (foundations.length)
|
|
191
|
+
result.foundations = foundations;
|
|
192
|
+
const advanced = parseMultilineList(body, "advanced");
|
|
193
|
+
if (advanced.length)
|
|
194
|
+
result.advanced = advanced;
|
|
195
|
+
if (fields.timeout)
|
|
196
|
+
result.timeout = fields.timeout;
|
|
197
|
+
if (fields["error-handler"])
|
|
198
|
+
result.errorHandler = fields["error-handler"];
|
|
199
|
+
if (fields.durable)
|
|
200
|
+
result.durable = fields.durable === "true";
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Parse the `orchestrator { ... }` block into an {@link OrchestratorNode}.
|
|
205
|
+
*/
|
|
206
|
+
export function parseOrchestrator(body) {
|
|
207
|
+
const fields = parseFields(body);
|
|
208
|
+
const handles = parseMultilineList(body, "handles");
|
|
209
|
+
const outputs = parseOutputsBlock(body);
|
|
210
|
+
const node = {
|
|
211
|
+
id: "orchestrator",
|
|
212
|
+
type: "orchestrator",
|
|
213
|
+
label: "Orchestrator",
|
|
214
|
+
model: fields.model ?? "unknown",
|
|
215
|
+
handles,
|
|
216
|
+
};
|
|
217
|
+
if (fields.generates)
|
|
218
|
+
node.generates = unquote(fields.generates);
|
|
219
|
+
if (outputs)
|
|
220
|
+
node.outputs = outputs;
|
|
221
|
+
return node;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Parse the `roles { ... }` block into a map of role name to description.
|
|
225
|
+
*/
|
|
226
|
+
export function parseRoles(body) {
|
|
227
|
+
const roles = {};
|
|
228
|
+
for (const line of body.split("\n")) {
|
|
229
|
+
const m = line.match(/^\s*([a-zA-Z_-]+)\s*:\s*"(.+?)"\s*$/);
|
|
230
|
+
if (m)
|
|
231
|
+
roles[m[1]] = m[2];
|
|
232
|
+
}
|
|
233
|
+
return roles;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Parse an `action <id> { ... }` block into an {@link ActionNode}.
|
|
237
|
+
*/
|
|
238
|
+
export function parseAction(id, body) {
|
|
239
|
+
const fields = parseFields(body);
|
|
240
|
+
const commands = parseMultilineList(body, "commands");
|
|
241
|
+
const node = {
|
|
242
|
+
id,
|
|
243
|
+
type: "action",
|
|
244
|
+
label: toLabel(id),
|
|
245
|
+
};
|
|
246
|
+
if (fields.kind)
|
|
247
|
+
node.kind = fields.kind;
|
|
248
|
+
if (fields.source)
|
|
249
|
+
node.source = unquote(fields.source);
|
|
250
|
+
if (fields.description)
|
|
251
|
+
node.description = unquote(fields.description);
|
|
252
|
+
if (commands.length)
|
|
253
|
+
node.commands = commands;
|
|
254
|
+
if (fields.timeout)
|
|
255
|
+
node.timeout = fields.timeout;
|
|
256
|
+
if (fields["on-fail"])
|
|
257
|
+
node.onFail = fields["on-fail"];
|
|
258
|
+
if (fields.join)
|
|
259
|
+
node.join = fields.join;
|
|
260
|
+
return node;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Parse an `agent <id> { ... }` block into an {@link AgentNode}.
|
|
264
|
+
*
|
|
265
|
+
* @param id - The agent identifier from the block header.
|
|
266
|
+
* @param body - The block body (between braces).
|
|
267
|
+
* @param roles - Role descriptions from the `roles` block, used to attach a
|
|
268
|
+
* role description to matching agents.
|
|
269
|
+
*/
|
|
270
|
+
export function parseAgent(id, body, roles) {
|
|
271
|
+
const fields = parseFields(body);
|
|
272
|
+
const tools = parseMultilineList(body, "tools");
|
|
273
|
+
const skills = parseMultilineList(body, "skills");
|
|
274
|
+
const reads = parseMultilineList(body, "reads");
|
|
275
|
+
const writes = parseMultilineList(body, "writes");
|
|
276
|
+
const disallowedTools = parseMultilineList(body, "disallowed-tools");
|
|
277
|
+
const outputs = parseOutputsBlock(body);
|
|
278
|
+
const node = {
|
|
279
|
+
id,
|
|
280
|
+
type: "agent",
|
|
281
|
+
label: toLabel(id),
|
|
282
|
+
};
|
|
283
|
+
if (fields.phase)
|
|
284
|
+
node.phase = parseFloat(fields.phase);
|
|
285
|
+
if (fields.model)
|
|
286
|
+
node.model = fields.model;
|
|
287
|
+
if (fields.permissions)
|
|
288
|
+
node.permissions = fields.permissions;
|
|
289
|
+
// Extract prompt from prompt {} block (not KV pair)
|
|
290
|
+
const promptBlock = extractBlock(body, "prompt");
|
|
291
|
+
if (promptBlock) {
|
|
292
|
+
node.prompt = dedentBlock(promptBlock.body);
|
|
293
|
+
}
|
|
294
|
+
if (tools.length)
|
|
295
|
+
node.tools = tools;
|
|
296
|
+
if (skills.length)
|
|
297
|
+
node.skills = skills;
|
|
298
|
+
if (reads.length)
|
|
299
|
+
node.reads = reads;
|
|
300
|
+
if (writes.length)
|
|
301
|
+
node.writes = writes;
|
|
302
|
+
if (disallowedTools.length)
|
|
303
|
+
node.disallowedTools = disallowedTools;
|
|
304
|
+
if (fields.skip)
|
|
305
|
+
node.skip = fields.skip;
|
|
306
|
+
if (fields.behavior)
|
|
307
|
+
node.behavior = fields.behavior;
|
|
308
|
+
if (fields.invocation)
|
|
309
|
+
node.invocation = fields.invocation;
|
|
310
|
+
// Retry: support both simple number and structured block form
|
|
311
|
+
const retryBlock = extractBlock(body, "retry");
|
|
312
|
+
if (retryBlock) {
|
|
313
|
+
const rFields = parseFields(retryBlock.body);
|
|
314
|
+
const retryConfig = {
|
|
315
|
+
max: rFields.max ? parseInt(rFields.max, 10) : 1,
|
|
316
|
+
};
|
|
317
|
+
if (rFields.backoff)
|
|
318
|
+
retryConfig.backoff = rFields.backoff;
|
|
319
|
+
if (rFields.interval)
|
|
320
|
+
retryConfig.interval = rFields.interval;
|
|
321
|
+
if (rFields["max-interval"])
|
|
322
|
+
retryConfig.maxInterval = rFields["max-interval"];
|
|
323
|
+
if (rFields.jitter)
|
|
324
|
+
retryConfig.jitter = rFields.jitter === "true";
|
|
325
|
+
const nonRetryable = parseMultilineList(retryBlock.body, "non-retryable");
|
|
326
|
+
if (nonRetryable.length)
|
|
327
|
+
retryConfig.nonRetryable = nonRetryable;
|
|
328
|
+
node.retry = retryConfig;
|
|
329
|
+
}
|
|
330
|
+
else if (fields.retry) {
|
|
331
|
+
node.retry = parseInt(fields.retry, 10);
|
|
332
|
+
}
|
|
333
|
+
// Timeout
|
|
334
|
+
if (fields.timeout)
|
|
335
|
+
node.timeout = fields.timeout;
|
|
336
|
+
// On-fail
|
|
337
|
+
if (fields["on-fail"])
|
|
338
|
+
node.onFail = fields["on-fail"];
|
|
339
|
+
// Sampling parameters
|
|
340
|
+
if (fields.temperature)
|
|
341
|
+
node.temperature = parseFloat(fields.temperature);
|
|
342
|
+
if (fields["max-tokens"])
|
|
343
|
+
node.maxTokens = parseInt(fields["max-tokens"], 10);
|
|
344
|
+
if (fields["top-p"])
|
|
345
|
+
node.topP = parseFloat(fields["top-p"]);
|
|
346
|
+
if (fields["top-k"])
|
|
347
|
+
node.topK = parseInt(fields["top-k"], 10);
|
|
348
|
+
const stop = parseMultilineList(body, "stop");
|
|
349
|
+
if (stop.length)
|
|
350
|
+
node.stop = stop;
|
|
351
|
+
if (fields.seed)
|
|
352
|
+
node.seed = parseInt(fields.seed, 10);
|
|
353
|
+
// Thinking
|
|
354
|
+
if (fields.thinking)
|
|
355
|
+
node.thinking = fields.thinking;
|
|
356
|
+
if (fields["thinking-budget"])
|
|
357
|
+
node.thinkingBudget = parseInt(fields["thinking-budget"], 10);
|
|
358
|
+
// Output format
|
|
359
|
+
if (fields["output-format"])
|
|
360
|
+
node.outputFormat = fields["output-format"];
|
|
361
|
+
// Log level
|
|
362
|
+
if (fields["log-level"])
|
|
363
|
+
node.logLevel = fields["log-level"];
|
|
364
|
+
// Join semantics
|
|
365
|
+
if (fields.join)
|
|
366
|
+
node.join = fields.join;
|
|
367
|
+
// Circuit breaker
|
|
368
|
+
const cbBlock = extractBlock(body, "circuit-breaker");
|
|
369
|
+
if (cbBlock) {
|
|
370
|
+
const cbFields = parseFields(cbBlock.body);
|
|
371
|
+
const cb = {
|
|
372
|
+
threshold: cbFields.threshold ? parseInt(cbFields.threshold, 10) : 1,
|
|
373
|
+
window: cbFields.window ?? "1m",
|
|
374
|
+
cooldown: cbFields.cooldown ?? "30s",
|
|
375
|
+
};
|
|
376
|
+
node.circuitBreaker = cb;
|
|
377
|
+
}
|
|
378
|
+
// Compensation / saga
|
|
379
|
+
if (fields.compensates)
|
|
380
|
+
node.compensates = fields.compensates;
|
|
381
|
+
if (fields.isolation)
|
|
382
|
+
node.isolation = fields.isolation;
|
|
383
|
+
if (fields.background)
|
|
384
|
+
node.background = fields.background === "true";
|
|
385
|
+
const mcpServers = parseMultilineList(body, "mcp-servers");
|
|
386
|
+
if (mcpServers.length)
|
|
387
|
+
node.mcpServers = mcpServers;
|
|
388
|
+
if (outputs)
|
|
389
|
+
node.outputs = outputs;
|
|
390
|
+
// Parse scale sub-block
|
|
391
|
+
const scaleBlock = extractBlock(body, "scale");
|
|
392
|
+
if (scaleBlock) {
|
|
393
|
+
const sFields = parseFields(scaleBlock.body);
|
|
394
|
+
const scale = {
|
|
395
|
+
mode: sFields.mode ?? "auto",
|
|
396
|
+
by: sFields.by ?? "",
|
|
397
|
+
min: sFields.min ? parseInt(sFields.min, 10) : 1,
|
|
398
|
+
max: sFields.max ? parseInt(sFields.max, 10) : 1,
|
|
399
|
+
batchSize: sFields["batch-size"]
|
|
400
|
+
? parseInt(sFields["batch-size"], 10)
|
|
401
|
+
: null,
|
|
402
|
+
};
|
|
403
|
+
node.scale = scale;
|
|
404
|
+
}
|
|
405
|
+
// Parse per-agent hooks block
|
|
406
|
+
const agentHooksBlock = extractBlock(body, "hooks");
|
|
407
|
+
if (agentHooksBlock) {
|
|
408
|
+
const agentHooks = [];
|
|
409
|
+
const hookBlocks = extractAllBlocks(agentHooksBlock.body, "hook");
|
|
410
|
+
for (const hBlock of hookBlocks) {
|
|
411
|
+
if (!hBlock.id)
|
|
412
|
+
continue;
|
|
413
|
+
const hFields = parseFields(hBlock.body);
|
|
414
|
+
const agentHook = {
|
|
415
|
+
name: hBlock.id,
|
|
416
|
+
on: hFields.on ?? "",
|
|
417
|
+
matcher: hFields.matcher ? unquote(hFields.matcher) : "",
|
|
418
|
+
run: hFields.run ? unquote(hFields.run) : "",
|
|
419
|
+
...(hFields.type ? { type: hFields.type } : {}),
|
|
420
|
+
...(hFields.timeout
|
|
421
|
+
? { timeout: parseInt(hFields.timeout, 10) }
|
|
422
|
+
: {}),
|
|
423
|
+
};
|
|
424
|
+
// Parse extensions sub-block inside per-agent hook
|
|
425
|
+
const hookExtensions = parseExtensionsBlock(hBlock.body);
|
|
426
|
+
if (hookExtensions)
|
|
427
|
+
agentHook.extensions = hookExtensions;
|
|
428
|
+
agentHooks.push(agentHook);
|
|
429
|
+
}
|
|
430
|
+
if (agentHooks.length > 0)
|
|
431
|
+
node.hooks = agentHooks;
|
|
432
|
+
}
|
|
433
|
+
// Sandbox override
|
|
434
|
+
if (fields.sandbox) {
|
|
435
|
+
const sv = fields.sandbox;
|
|
436
|
+
if (sv === "true")
|
|
437
|
+
node.sandbox = true;
|
|
438
|
+
else if (sv === "false")
|
|
439
|
+
node.sandbox = false;
|
|
440
|
+
else
|
|
441
|
+
node.sandbox = sv;
|
|
442
|
+
}
|
|
443
|
+
// Fallback chain
|
|
444
|
+
const fallbackChain = parseMultilineList(body, "fallback-chain");
|
|
445
|
+
if (fallbackChain.length)
|
|
446
|
+
node.fallbackChain = fallbackChain;
|
|
447
|
+
// Input/output schema blocks
|
|
448
|
+
const inputSchemaBlock = extractBlock(body, "input-schema");
|
|
449
|
+
if (inputSchemaBlock) {
|
|
450
|
+
node.inputSchema = parseSchemaFields(inputSchemaBlock.body);
|
|
451
|
+
}
|
|
452
|
+
const outputSchemaBlock = extractBlock(body, "output-schema");
|
|
453
|
+
if (outputSchemaBlock) {
|
|
454
|
+
node.outputSchema = parseSchemaFields(outputSchemaBlock.body);
|
|
455
|
+
}
|
|
456
|
+
// Artifact lineage: produces and consumes
|
|
457
|
+
const produces = parseMultilineList(body, "produces");
|
|
458
|
+
if (produces.length)
|
|
459
|
+
node.produces = produces;
|
|
460
|
+
const consumes = parseMultilineList(body, "consumes");
|
|
461
|
+
if (consumes.length)
|
|
462
|
+
node.consumes = consumes;
|
|
463
|
+
// Prompt variants (A/B testing)
|
|
464
|
+
const variantsBlock = extractBlock(body, "variants");
|
|
465
|
+
if (variantsBlock) {
|
|
466
|
+
const variants = [];
|
|
467
|
+
const variantBlocks = extractAllBlocks(variantsBlock.body, "variant");
|
|
468
|
+
for (const vBlock of variantBlocks) {
|
|
469
|
+
if (!vBlock.id)
|
|
470
|
+
continue;
|
|
471
|
+
const vFields = parseFields(vBlock.body);
|
|
472
|
+
const variant = {
|
|
473
|
+
id: vBlock.id,
|
|
474
|
+
weight: vFields.weight ? parseFloat(vFields.weight) : 0,
|
|
475
|
+
};
|
|
476
|
+
const vPromptBlock = extractBlock(vBlock.body, "prompt");
|
|
477
|
+
if (vPromptBlock) {
|
|
478
|
+
variant.prompt = dedentBlock(vPromptBlock.body);
|
|
479
|
+
}
|
|
480
|
+
if (vFields.temperature)
|
|
481
|
+
variant.temperature = parseFloat(vFields.temperature);
|
|
482
|
+
if (vFields.model)
|
|
483
|
+
variant.model = vFields.model;
|
|
484
|
+
variants.push(variant);
|
|
485
|
+
}
|
|
486
|
+
if (variants.length > 0)
|
|
487
|
+
node.variants = variants;
|
|
488
|
+
}
|
|
489
|
+
// Rate limit
|
|
490
|
+
if (fields["rate-limit"])
|
|
491
|
+
node.rateLimit = fields["rate-limit"];
|
|
492
|
+
// New fields: description, max-turns, extensions
|
|
493
|
+
if (fields.description)
|
|
494
|
+
node.description = unquote(fields.description);
|
|
495
|
+
if (fields["max-turns"])
|
|
496
|
+
node.maxTurns = parseInt(fields["max-turns"], 10);
|
|
497
|
+
const extensions = parseExtensionsBlock(body);
|
|
498
|
+
if (extensions)
|
|
499
|
+
node.extensions = extensions;
|
|
500
|
+
// Attach role description if available.
|
|
501
|
+
// Prefer exact match, then longest prefix match.
|
|
502
|
+
const exactRole = roles[id];
|
|
503
|
+
if (exactRole) {
|
|
504
|
+
node.role = exactRole;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
const prefixMatches = Object.keys(roles)
|
|
508
|
+
.filter((r) => id.startsWith(r))
|
|
509
|
+
.sort((a, b) => b.length - a.length);
|
|
510
|
+
if (prefixMatches.length > 0)
|
|
511
|
+
node.role = roles[prefixMatches[0]];
|
|
512
|
+
}
|
|
513
|
+
return node;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Parse a `gate <id> { ... }` block into a {@link GateNode}.
|
|
517
|
+
*/
|
|
518
|
+
export function parseGate(id, body) {
|
|
519
|
+
const fields = parseFields(body);
|
|
520
|
+
const checks = parseMultilineList(body, "checks");
|
|
521
|
+
const node = {
|
|
522
|
+
id,
|
|
523
|
+
type: "gate",
|
|
524
|
+
label: toLabel(id),
|
|
525
|
+
};
|
|
526
|
+
if (fields.after)
|
|
527
|
+
node.after = fields.after;
|
|
528
|
+
if (fields.before)
|
|
529
|
+
node.before = fields.before;
|
|
530
|
+
if (fields.run)
|
|
531
|
+
node.run = unquote(fields.run);
|
|
532
|
+
if (checks.length)
|
|
533
|
+
node.checks = checks;
|
|
534
|
+
if (fields.retry)
|
|
535
|
+
node.retry = parseInt(fields.retry, 10);
|
|
536
|
+
if (fields["on-fail"])
|
|
537
|
+
node.onFail = fields["on-fail"];
|
|
538
|
+
if (fields.behavior)
|
|
539
|
+
node.behavior = fields.behavior;
|
|
540
|
+
if (fields.timeout)
|
|
541
|
+
node.timeout = fields.timeout;
|
|
542
|
+
const extensions = parseExtensionsBlock(body);
|
|
543
|
+
if (extensions)
|
|
544
|
+
node.extensions = extensions;
|
|
545
|
+
return node;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Parse a `human <id> { ... }` block into a {@link HumanNode}.
|
|
549
|
+
*/
|
|
550
|
+
export function parseHuman(id, body) {
|
|
551
|
+
const fields = parseFields(body);
|
|
552
|
+
const node = {
|
|
553
|
+
id,
|
|
554
|
+
type: "human",
|
|
555
|
+
label: toLabel(id),
|
|
556
|
+
};
|
|
557
|
+
if (fields.description)
|
|
558
|
+
node.description = unquote(fields.description);
|
|
559
|
+
if (fields.timeout)
|
|
560
|
+
node.timeout = fields.timeout;
|
|
561
|
+
if (fields["on-timeout"])
|
|
562
|
+
node.onTimeout = fields["on-timeout"];
|
|
563
|
+
return node;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Parse a `group <id> { ... }` block into a {@link GroupNode}.
|
|
567
|
+
*/
|
|
568
|
+
export function parseGroup(id, body) {
|
|
569
|
+
const fields = parseFields(body);
|
|
570
|
+
const members = parseList(fields.members ?? "");
|
|
571
|
+
const node = {
|
|
572
|
+
id,
|
|
573
|
+
type: "group",
|
|
574
|
+
label: toLabel(id),
|
|
575
|
+
members,
|
|
576
|
+
};
|
|
577
|
+
if (fields["speaker-selection"])
|
|
578
|
+
node.speakerSelection = fields["speaker-selection"];
|
|
579
|
+
if (fields["max-rounds"])
|
|
580
|
+
node.maxRounds = parseInt(fields["max-rounds"], 10);
|
|
581
|
+
if (fields.termination)
|
|
582
|
+
node.termination = unquote(fields.termination);
|
|
583
|
+
if (fields.description)
|
|
584
|
+
node.description = unquote(fields.description);
|
|
585
|
+
if (fields.timeout)
|
|
586
|
+
node.timeout = fields.timeout;
|
|
587
|
+
return node;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Parse the `flow { ... }` block into an array of {@link EdgeDef}.
|
|
591
|
+
*
|
|
592
|
+
* Supports chain syntax (`a -> b -> c`), fan-out (`a -> [b, c]`),
|
|
593
|
+
* error edges (`-x->`, `-x[type]->`), and edge annotations
|
|
594
|
+
* (`[when condition, max N, tolerance: N, race, wait 30s]`).
|
|
595
|
+
*/
|
|
596
|
+
export function parseFlow(body, _edgeAttributeErrors) {
|
|
597
|
+
const edges = [];
|
|
598
|
+
for (const line of body.split("\n")) {
|
|
599
|
+
const trimmed = line.trim();
|
|
600
|
+
if (!trimmed)
|
|
601
|
+
continue;
|
|
602
|
+
let condition = null;
|
|
603
|
+
let maxIterations = null;
|
|
604
|
+
let per = null;
|
|
605
|
+
let tolerance;
|
|
606
|
+
let race;
|
|
607
|
+
let wait;
|
|
608
|
+
let weight;
|
|
609
|
+
let reflection;
|
|
610
|
+
let flowPart = trimmed;
|
|
611
|
+
// Match annotation at end of line: [when ..., max N, per agent-id, tolerance: N, race, wait 30s, reflection]
|
|
612
|
+
// Must start with "when", "max", "per", "tolerance", "race", "wait", "join", "weight", or "reflection" to avoid matching fan-out lists.
|
|
613
|
+
const bracketMatch = trimmed.match(/\[(when\s+.+?|max\s+\d+.*?|per\s+\S+.*?|tolerance\s*:.+?|race.*?|wait\s+\S+.*?|join\s+\S+.*?|weight\s+\S+.*?|reflection.*?)\]\s*$/);
|
|
614
|
+
if (bracketMatch) {
|
|
615
|
+
flowPart = trimmed.slice(0, bracketMatch.index).trim();
|
|
616
|
+
const annotation = bracketMatch[1];
|
|
617
|
+
// V12: Detect wrong attribute order — "max" before "when"
|
|
618
|
+
const maxIdx = annotation.search(/\bmax\s+\d+/);
|
|
619
|
+
const whenIdx = annotation.search(/\bwhen\s+/);
|
|
620
|
+
if (maxIdx !== -1 && whenIdx !== -1 && maxIdx < whenIdx && _edgeAttributeErrors) {
|
|
621
|
+
_edgeAttributeErrors.push({
|
|
622
|
+
rule: "V12",
|
|
623
|
+
level: "error",
|
|
624
|
+
message: `Edge annotation has wrong attribute order: "max" before "when" — expected [when ..., max N] (line: ${trimmed})`,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
const whenMatch = annotation.match(/when\s+(.+?)(?:,\s*(?:max|per|tolerance|race|wait|weight|reflection)|$)/);
|
|
628
|
+
if (whenMatch)
|
|
629
|
+
condition = whenMatch[1].trim();
|
|
630
|
+
const maxMatch = annotation.match(/max\s+(\d+)/);
|
|
631
|
+
if (maxMatch)
|
|
632
|
+
maxIterations = parseInt(maxMatch[1], 10);
|
|
633
|
+
const perMatch = annotation.match(/per\s+(\S+)/);
|
|
634
|
+
if (perMatch)
|
|
635
|
+
per = perMatch[1].replace(/,\s*$/, "");
|
|
636
|
+
// tolerance: N or tolerance: 33%
|
|
637
|
+
const toleranceMatch = annotation.match(/tolerance\s*:\s*(\d+%?)/);
|
|
638
|
+
if (toleranceMatch) {
|
|
639
|
+
const tolVal = toleranceMatch[1];
|
|
640
|
+
tolerance = tolVal.endsWith("%") ? tolVal : parseInt(tolVal, 10);
|
|
641
|
+
}
|
|
642
|
+
// race
|
|
643
|
+
if (/\brace\b/.test(annotation)) {
|
|
644
|
+
race = true;
|
|
645
|
+
}
|
|
646
|
+
// wait 30s
|
|
647
|
+
const waitMatch = annotation.match(/wait\s+(\d+[smhd])/);
|
|
648
|
+
if (waitMatch) {
|
|
649
|
+
wait = waitMatch[1];
|
|
650
|
+
}
|
|
651
|
+
// weight 0.7
|
|
652
|
+
const weightMatch = annotation.match(/weight\s+(\d+(?:\.\d+)?)/);
|
|
653
|
+
if (weightMatch) {
|
|
654
|
+
weight = parseFloat(weightMatch[1]);
|
|
655
|
+
}
|
|
656
|
+
// reflection
|
|
657
|
+
if (/\breflection\b/.test(annotation)) {
|
|
658
|
+
reflection = true;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// Split the flow part into segments, handling error edge arrows.
|
|
662
|
+
// We need to split on both -> and -x-> / -x[type]->.
|
|
663
|
+
// Strategy: use regex to split on arrow patterns, tracking which type each is.
|
|
664
|
+
const segments = [];
|
|
665
|
+
const arrowPattern = /-x\[([^\]]+)\]->|-x->|->/g;
|
|
666
|
+
let lastIndex = 0;
|
|
667
|
+
let arrowMatch;
|
|
668
|
+
const arrowTypes = [];
|
|
669
|
+
// Reset lastIndex
|
|
670
|
+
arrowPattern.lastIndex = 0;
|
|
671
|
+
while ((arrowMatch = arrowPattern.exec(flowPart)) !== null) {
|
|
672
|
+
const before = flowPart.slice(lastIndex, arrowMatch.index).trim();
|
|
673
|
+
if (before) {
|
|
674
|
+
segments.push({ text: before, isError: false });
|
|
675
|
+
}
|
|
676
|
+
if (arrowMatch[0].startsWith("-x")) {
|
|
677
|
+
// Error edge
|
|
678
|
+
if (arrowMatch[1]) {
|
|
679
|
+
// -x[type]->
|
|
680
|
+
arrowTypes.push({ isError: true, errorType: arrowMatch[1] });
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
// -x->
|
|
684
|
+
arrowTypes.push({ isError: true });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
// Normal ->
|
|
689
|
+
arrowTypes.push({ isError: false });
|
|
690
|
+
}
|
|
691
|
+
lastIndex = arrowMatch.index + arrowMatch[0].length;
|
|
692
|
+
}
|
|
693
|
+
// Remaining text after last arrow
|
|
694
|
+
const remaining = flowPart.slice(lastIndex).trim();
|
|
695
|
+
if (remaining) {
|
|
696
|
+
segments.push({ text: remaining, isError: false });
|
|
697
|
+
}
|
|
698
|
+
// Now segments[i] and arrowTypes[i] give us: segments[i] --arrowTypes[i]--> segments[i+1]
|
|
699
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
700
|
+
const from = segments[i].text;
|
|
701
|
+
const to = segments[i + 1].text;
|
|
702
|
+
const arrow = arrowTypes[i];
|
|
703
|
+
const isLastSegment = i === segments.length - 2;
|
|
704
|
+
// Check for fan-out: [a, b, c]
|
|
705
|
+
const fanOutMatch = to.match(/^\[([^\]]+)\]$/);
|
|
706
|
+
// Build base edge properties
|
|
707
|
+
const baseEdge = {
|
|
708
|
+
condition: isLastSegment ? condition : null,
|
|
709
|
+
maxIterations: isLastSegment ? maxIterations : null,
|
|
710
|
+
per: isLastSegment ? per : null,
|
|
711
|
+
};
|
|
712
|
+
if (arrow.isError) {
|
|
713
|
+
baseEdge.isError = true;
|
|
714
|
+
if (arrow.errorType)
|
|
715
|
+
baseEdge.errorType = arrow.errorType;
|
|
716
|
+
}
|
|
717
|
+
if (isLastSegment && tolerance !== undefined)
|
|
718
|
+
baseEdge.tolerance = tolerance;
|
|
719
|
+
if (isLastSegment && race)
|
|
720
|
+
baseEdge.race = true;
|
|
721
|
+
if (isLastSegment && wait)
|
|
722
|
+
baseEdge.wait = wait;
|
|
723
|
+
if (isLastSegment && weight !== undefined)
|
|
724
|
+
baseEdge.weight = weight;
|
|
725
|
+
if (isLastSegment && reflection)
|
|
726
|
+
baseEdge.reflection = true;
|
|
727
|
+
if (fanOutMatch) {
|
|
728
|
+
const targets = fanOutMatch[1].split(",").map((s) => s.trim());
|
|
729
|
+
for (const target of targets) {
|
|
730
|
+
edges.push({
|
|
731
|
+
from,
|
|
732
|
+
to: target,
|
|
733
|
+
condition: baseEdge.condition ?? null,
|
|
734
|
+
maxIterations: baseEdge.maxIterations ?? null,
|
|
735
|
+
per: baseEdge.per ?? null,
|
|
736
|
+
...(baseEdge.isError ? { isError: true } : {}),
|
|
737
|
+
...(baseEdge.errorType ? { errorType: baseEdge.errorType } : {}),
|
|
738
|
+
...(baseEdge.tolerance !== undefined ? { tolerance: baseEdge.tolerance } : {}),
|
|
739
|
+
...(baseEdge.race ? { race: true } : {}),
|
|
740
|
+
...(baseEdge.wait ? { wait: baseEdge.wait } : {}),
|
|
741
|
+
...(baseEdge.weight !== undefined ? { weight: baseEdge.weight } : {}),
|
|
742
|
+
...(baseEdge.reflection ? { reflection: true } : {}),
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
edges.push({
|
|
748
|
+
from,
|
|
749
|
+
to,
|
|
750
|
+
condition: baseEdge.condition ?? null,
|
|
751
|
+
maxIterations: baseEdge.maxIterations ?? null,
|
|
752
|
+
per: baseEdge.per ?? null,
|
|
753
|
+
...(baseEdge.isError ? { isError: true } : {}),
|
|
754
|
+
...(baseEdge.errorType ? { errorType: baseEdge.errorType } : {}),
|
|
755
|
+
...(baseEdge.tolerance !== undefined ? { tolerance: baseEdge.tolerance } : {}),
|
|
756
|
+
...(baseEdge.race ? { race: true } : {}),
|
|
757
|
+
...(baseEdge.wait ? { wait: baseEdge.wait } : {}),
|
|
758
|
+
...(baseEdge.weight !== undefined ? { weight: baseEdge.weight } : {}),
|
|
759
|
+
...(baseEdge.reflection ? { reflection: true } : {}),
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return edges;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Parse the `depth { ... }` block.
|
|
768
|
+
*/
|
|
769
|
+
export function parseDepth(body) {
|
|
770
|
+
const factors = parseMultilineList(body, "factors");
|
|
771
|
+
const levels = [];
|
|
772
|
+
const levelRe = /level\s+(\d+)\s+"([^"]+)"\s*\{([^}]*)\}/g;
|
|
773
|
+
let m;
|
|
774
|
+
while ((m = levelRe.exec(body)) !== null) {
|
|
775
|
+
const omit = parseMultilineList(m[3], "omit");
|
|
776
|
+
levels.push({
|
|
777
|
+
level: parseInt(m[1], 10),
|
|
778
|
+
label: m[2],
|
|
779
|
+
omit,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
return { factors, levels };
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Parse the `memory { ... }` block.
|
|
786
|
+
*
|
|
787
|
+
* Extracts known sub-blocks (domains, references, external-docs, metrics,
|
|
788
|
+
* workspace) and returns them as a nested record.
|
|
789
|
+
*/
|
|
790
|
+
export function parseMemory(body, _unknownSubBlockWarnings) {
|
|
791
|
+
const memory = {};
|
|
792
|
+
const knownSubs = [
|
|
793
|
+
"domains",
|
|
794
|
+
"references",
|
|
795
|
+
"external-docs",
|
|
796
|
+
"metrics",
|
|
797
|
+
"workspace",
|
|
798
|
+
];
|
|
799
|
+
const knownSubsSet = new Set(knownSubs);
|
|
800
|
+
for (const name of knownSubs) {
|
|
801
|
+
const block = extractBlock(body, name);
|
|
802
|
+
if (!block)
|
|
803
|
+
continue;
|
|
804
|
+
const fields = parseFields(block.body);
|
|
805
|
+
const entry = {};
|
|
806
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
807
|
+
entry[k] = v.startsWith("[") ? parseList(v) : unquote(v);
|
|
808
|
+
}
|
|
809
|
+
// Check for multiline lists
|
|
810
|
+
for (const key of ["blueprints", "files", "structure"]) {
|
|
811
|
+
const list = parseMultilineList(block.body, key);
|
|
812
|
+
if (list.length)
|
|
813
|
+
entry[key] = list;
|
|
814
|
+
}
|
|
815
|
+
memory[name] = entry;
|
|
816
|
+
}
|
|
817
|
+
// Detect unknown sub-blocks
|
|
818
|
+
if (_unknownSubBlockWarnings) {
|
|
819
|
+
const subBlockRe = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
|
|
820
|
+
let m;
|
|
821
|
+
while ((m = subBlockRe.exec(body)) !== null) {
|
|
822
|
+
const name = m[1];
|
|
823
|
+
if (!knownSubsSet.has(name)) {
|
|
824
|
+
_unknownSubBlockWarnings.push({
|
|
825
|
+
rule: "V24",
|
|
826
|
+
level: "warning",
|
|
827
|
+
message: `Unknown sub-block "${name}" in memory block — known sub-blocks are: ${knownSubs.join(", ")}`,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return memory;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Parse the `batch { ... }` block.
|
|
836
|
+
*/
|
|
837
|
+
export function parseBatch(body) {
|
|
838
|
+
const fields = parseFields(body);
|
|
839
|
+
const result = {};
|
|
840
|
+
if (fields.parallel)
|
|
841
|
+
result.parallel = fields.parallel === "true";
|
|
842
|
+
if (fields.per)
|
|
843
|
+
result.per = fields.per;
|
|
844
|
+
if (fields.workspace)
|
|
845
|
+
result.workspace = unquote(fields.workspace);
|
|
846
|
+
// Parse conflicts sub-block
|
|
847
|
+
const conflictsBlock = extractBlock(body, "conflicts");
|
|
848
|
+
if (conflictsBlock) {
|
|
849
|
+
const cFields = parseFields(conflictsBlock.body);
|
|
850
|
+
const detect = parseMultilineList(conflictsBlock.body, "detect");
|
|
851
|
+
result.conflicts = {
|
|
852
|
+
detect,
|
|
853
|
+
resolve: cFields.resolve ?? null,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Parse the `environments { ... }` block.
|
|
860
|
+
*
|
|
861
|
+
* Extracts named environment sub-blocks. Handles any environment name,
|
|
862
|
+
* not just "staging" and "production".
|
|
863
|
+
*/
|
|
864
|
+
export function parseEnvironments(body) {
|
|
865
|
+
const envs = {};
|
|
866
|
+
// Extract all named sub-blocks generically
|
|
867
|
+
const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
|
|
868
|
+
let m;
|
|
869
|
+
while ((m = re.exec(body)) !== null) {
|
|
870
|
+
const envName = m[1];
|
|
871
|
+
const startIdx = m.index + m[0].length;
|
|
872
|
+
let depth = 1;
|
|
873
|
+
let i = startIdx;
|
|
874
|
+
while (i < body.length && depth > 0) {
|
|
875
|
+
if (body[i] === "{")
|
|
876
|
+
depth++;
|
|
877
|
+
else if (body[i] === "}")
|
|
878
|
+
depth--;
|
|
879
|
+
i++;
|
|
880
|
+
}
|
|
881
|
+
if (depth === 0) {
|
|
882
|
+
const innerBody = body.slice(startIdx, i - 1);
|
|
883
|
+
const fields = parseFields(innerBody);
|
|
884
|
+
const env = {};
|
|
885
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
886
|
+
env[k] = unquote(v);
|
|
887
|
+
}
|
|
888
|
+
envs[envName] = env;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return envs;
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Parse the `triggers { ... }` block.
|
|
895
|
+
*/
|
|
896
|
+
export function parseTriggers(body) {
|
|
897
|
+
const triggers = [];
|
|
898
|
+
const cmdBlocks = extractAllBlocks(body, "command");
|
|
899
|
+
for (const block of cmdBlocks) {
|
|
900
|
+
if (!block.id)
|
|
901
|
+
continue;
|
|
902
|
+
const fields = parseFields(block.body);
|
|
903
|
+
const trigger = {
|
|
904
|
+
name: block.id,
|
|
905
|
+
pattern: fields.pattern ? unquote(fields.pattern) : "",
|
|
906
|
+
};
|
|
907
|
+
if (fields.argument)
|
|
908
|
+
trigger.argument = fields.argument;
|
|
909
|
+
triggers.push(trigger);
|
|
910
|
+
}
|
|
911
|
+
return triggers;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Parse the global `hooks { ... }` block.
|
|
915
|
+
*/
|
|
916
|
+
export function parseHooks(body) {
|
|
917
|
+
const hooks = [];
|
|
918
|
+
const hookBlocks = extractAllBlocks(body, "hook");
|
|
919
|
+
for (const block of hookBlocks) {
|
|
920
|
+
if (!block.id)
|
|
921
|
+
continue;
|
|
922
|
+
const fields = parseFields(block.body);
|
|
923
|
+
const hook = {
|
|
924
|
+
name: block.id,
|
|
925
|
+
on: fields.on ?? "",
|
|
926
|
+
matcher: fields.matcher ? unquote(fields.matcher) : "",
|
|
927
|
+
run: fields.run ? unquote(fields.run) : "",
|
|
928
|
+
...(fields.type ? { type: fields.type } : {}),
|
|
929
|
+
...(fields.timeout
|
|
930
|
+
? { timeout: parseInt(fields.timeout, 10) }
|
|
931
|
+
: {}),
|
|
932
|
+
};
|
|
933
|
+
// Parse extensions sub-block inside hook
|
|
934
|
+
const extensions = parseExtensionsBlock(block.body);
|
|
935
|
+
if (extensions)
|
|
936
|
+
hook.extensions = extensions;
|
|
937
|
+
hooks.push(hook);
|
|
938
|
+
}
|
|
939
|
+
return hooks;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Parse the `settings { ... }` block.
|
|
943
|
+
*/
|
|
944
|
+
export function parseSettings(body) {
|
|
945
|
+
const result = {};
|
|
946
|
+
for (const key of ["allow", "deny", "ask"]) {
|
|
947
|
+
const list = parseMultilineList(body, key);
|
|
948
|
+
result[key] = list;
|
|
949
|
+
}
|
|
950
|
+
// Sandbox setting
|
|
951
|
+
const fields = parseFields(body);
|
|
952
|
+
if (fields.sandbox) {
|
|
953
|
+
const sv = fields.sandbox;
|
|
954
|
+
if (sv === "true")
|
|
955
|
+
result.sandbox = true;
|
|
956
|
+
else if (sv === "false")
|
|
957
|
+
result.sandbox = false;
|
|
958
|
+
else
|
|
959
|
+
result.sandbox = sv;
|
|
960
|
+
}
|
|
961
|
+
// Fallback chain
|
|
962
|
+
const fallbackChain = parseMultilineList(body, "fallback-chain");
|
|
963
|
+
if (fallbackChain.length)
|
|
964
|
+
result.fallbackChain = fallbackChain;
|
|
965
|
+
return result;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Parse the `metering { ... }` block.
|
|
969
|
+
*/
|
|
970
|
+
export function parseMetering(body) {
|
|
971
|
+
const fields = parseFields(body);
|
|
972
|
+
const track = parseMultilineList(body, "track");
|
|
973
|
+
const per = parseMultilineList(body, "per");
|
|
974
|
+
if (track.length === 0 && !fields.output)
|
|
975
|
+
return null;
|
|
976
|
+
return {
|
|
977
|
+
track,
|
|
978
|
+
per,
|
|
979
|
+
output: fields.output ? unquote(fields.output) : "",
|
|
980
|
+
format: fields.format ?? "",
|
|
981
|
+
pricing: fields.pricing ?? "",
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Parse the `mcp-servers { ... }` block.
|
|
986
|
+
*/
|
|
987
|
+
export function parseMcpServers(body) {
|
|
988
|
+
const servers = {};
|
|
989
|
+
const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
|
|
990
|
+
let m;
|
|
991
|
+
while ((m = re.exec(body)) !== null) {
|
|
992
|
+
const name = m[1];
|
|
993
|
+
const startIdx = m.index + m[0].length;
|
|
994
|
+
let depth = 1;
|
|
995
|
+
let i = startIdx;
|
|
996
|
+
while (i < body.length && depth > 0) {
|
|
997
|
+
if (body[i] === "{")
|
|
998
|
+
depth++;
|
|
999
|
+
else if (body[i] === "}")
|
|
1000
|
+
depth--;
|
|
1001
|
+
i++;
|
|
1002
|
+
}
|
|
1003
|
+
if (depth === 0) {
|
|
1004
|
+
const innerBody = body.slice(startIdx, i - 1);
|
|
1005
|
+
const fields = parseFields(innerBody);
|
|
1006
|
+
const server = {};
|
|
1007
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
1008
|
+
server[k] = unquote(v);
|
|
1009
|
+
}
|
|
1010
|
+
const args = parseMultilineList(innerBody, "args");
|
|
1011
|
+
if (args.length)
|
|
1012
|
+
server.args = args;
|
|
1013
|
+
// Parse env sub-block (key-value pairs for environment variables)
|
|
1014
|
+
const envBlock = extractBlock(innerBody, "env");
|
|
1015
|
+
if (envBlock) {
|
|
1016
|
+
const envVars = {};
|
|
1017
|
+
for (const line of envBlock.body.split("\n")) {
|
|
1018
|
+
const kv = parseKV(line);
|
|
1019
|
+
if (kv) {
|
|
1020
|
+
envVars[kv[0]] = unquote(kv[1]);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (Object.keys(envVars).length > 0) {
|
|
1024
|
+
server.env = envVars;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
servers[name] = server;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return servers;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Parse `skill <id> { ... }` blocks.
|
|
1034
|
+
*/
|
|
1035
|
+
export function parseSkillBlocks(topBody) {
|
|
1036
|
+
const skillBlocks = extractAllBlocks(topBody, "skill");
|
|
1037
|
+
const skillDefs = [];
|
|
1038
|
+
for (const block of skillBlocks) {
|
|
1039
|
+
if (!block.id)
|
|
1040
|
+
continue;
|
|
1041
|
+
const sFields = parseFields(block.body);
|
|
1042
|
+
const sd = {
|
|
1043
|
+
id: block.id,
|
|
1044
|
+
description: sFields.description ? unquote(sFields.description) : "",
|
|
1045
|
+
};
|
|
1046
|
+
const scripts = parseMultilineList(block.body, "scripts");
|
|
1047
|
+
const domains = parseMultilineList(block.body, "domains");
|
|
1048
|
+
const references = parseMultilineList(block.body, "references");
|
|
1049
|
+
if (scripts.length)
|
|
1050
|
+
sd.scripts = scripts;
|
|
1051
|
+
if (domains.length)
|
|
1052
|
+
sd.domains = domains;
|
|
1053
|
+
if (references.length)
|
|
1054
|
+
sd.references = references;
|
|
1055
|
+
if (sFields.prompt)
|
|
1056
|
+
sd.prompt = unquote(sFields.prompt);
|
|
1057
|
+
// New skill fields
|
|
1058
|
+
if (sFields["disable-model-invocation"]) {
|
|
1059
|
+
sd.disableModelInvocation = sFields["disable-model-invocation"] === "true";
|
|
1060
|
+
}
|
|
1061
|
+
if (sFields["user-invocable"]) {
|
|
1062
|
+
sd.userInvocable = sFields["user-invocable"] === "true";
|
|
1063
|
+
}
|
|
1064
|
+
if (sFields.context)
|
|
1065
|
+
sd.context = sFields.context;
|
|
1066
|
+
if (sFields.agent)
|
|
1067
|
+
sd.agent = sFields.agent;
|
|
1068
|
+
const allowedTools = parseMultilineList(block.body, "allowed-tools");
|
|
1069
|
+
if (allowedTools.length)
|
|
1070
|
+
sd.allowedTools = allowedTools;
|
|
1071
|
+
const skillExtensions = parseExtensionsBlock(block.body);
|
|
1072
|
+
if (skillExtensions)
|
|
1073
|
+
sd.extensions = skillExtensions;
|
|
1074
|
+
skillDefs.push(sd);
|
|
1075
|
+
}
|
|
1076
|
+
return skillDefs;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Parse the `context { ... }` block.
|
|
1080
|
+
*
|
|
1081
|
+
* Returns context file configuration with optional file path and includes list.
|
|
1082
|
+
*/
|
|
1083
|
+
export function parseContext(body) {
|
|
1084
|
+
const fields = parseFields(body);
|
|
1085
|
+
const result = {};
|
|
1086
|
+
if (fields.file)
|
|
1087
|
+
result.file = unquote(fields.file);
|
|
1088
|
+
const includes = parseMultilineList(body, "includes");
|
|
1089
|
+
if (includes.length)
|
|
1090
|
+
result.includes = includes;
|
|
1091
|
+
return result;
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Parse the `env { ... }` block.
|
|
1095
|
+
*
|
|
1096
|
+
* Returns a record of environment variable key-value pairs. Values may be
|
|
1097
|
+
* plain strings (backward compatible) or {@link SensitiveValue} objects when
|
|
1098
|
+
* the `sensitive` or `secret` modifier is used.
|
|
1099
|
+
*/
|
|
1100
|
+
export function parseEnv(body) {
|
|
1101
|
+
const result = {};
|
|
1102
|
+
for (const line of body.split("\n")) {
|
|
1103
|
+
const kv = parseKV(line);
|
|
1104
|
+
if (kv) {
|
|
1105
|
+
const rawValue = kv[1].trim();
|
|
1106
|
+
// Check for `secret "uri"` modifier
|
|
1107
|
+
if (rawValue.startsWith("secret ")) {
|
|
1108
|
+
const uriPart = rawValue.slice("secret ".length).trim();
|
|
1109
|
+
const uri = unquote(uriPart);
|
|
1110
|
+
const schemeMatch = uri.match(/^([a-zA-Z][a-zA-Z0-9]*):\/\//);
|
|
1111
|
+
const scheme = schemeMatch ? schemeMatch[1] : "";
|
|
1112
|
+
const secretRef = { scheme, uri };
|
|
1113
|
+
result[kv[0]] = {
|
|
1114
|
+
value: uri,
|
|
1115
|
+
sensitive: true,
|
|
1116
|
+
secretRef,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
// Check for `encrypted` modifier
|
|
1120
|
+
else if (rawValue.startsWith("encrypted ")) {
|
|
1121
|
+
const innerValue = rawValue.slice("encrypted ".length).trim();
|
|
1122
|
+
const unquoted = unquote(innerValue);
|
|
1123
|
+
const sv = {
|
|
1124
|
+
value: unquoted,
|
|
1125
|
+
sensitive: true,
|
|
1126
|
+
encrypted: true,
|
|
1127
|
+
};
|
|
1128
|
+
// Parse SOPS envelope: ENC[METHOD,data:...]
|
|
1129
|
+
const sopsMatch = unquoted.match(/^ENC\[([A-Z0-9_]+),data:/);
|
|
1130
|
+
if (sopsMatch) {
|
|
1131
|
+
sv.sopsMethod = sopsMatch[1];
|
|
1132
|
+
}
|
|
1133
|
+
result[kv[0]] = sv;
|
|
1134
|
+
}
|
|
1135
|
+
// Check for `sensitive` modifier
|
|
1136
|
+
else if (rawValue.startsWith("sensitive ")) {
|
|
1137
|
+
const innerValue = rawValue.slice("sensitive ".length).trim();
|
|
1138
|
+
result[kv[0]] = {
|
|
1139
|
+
value: unquote(innerValue),
|
|
1140
|
+
sensitive: true,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
// Plain string (backward compatible)
|
|
1144
|
+
else {
|
|
1145
|
+
result[kv[0]] = unquote(rawValue);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return result;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Parse the `providers { ... }` block.
|
|
1153
|
+
*
|
|
1154
|
+
* Each named sub-block declares a provider with api-key, base-url, models,
|
|
1155
|
+
* default, and any extra fields.
|
|
1156
|
+
*/
|
|
1157
|
+
export function parseProviders(body) {
|
|
1158
|
+
const providers = [];
|
|
1159
|
+
// Extract each named sub-block inside providers
|
|
1160
|
+
const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
|
|
1161
|
+
let m;
|
|
1162
|
+
while ((m = re.exec(body)) !== null) {
|
|
1163
|
+
const name = m[1];
|
|
1164
|
+
const startIdx = m.index + m[0].length;
|
|
1165
|
+
let depth = 1;
|
|
1166
|
+
let i = startIdx;
|
|
1167
|
+
while (i < body.length && depth > 0) {
|
|
1168
|
+
if (body[i] === "{")
|
|
1169
|
+
depth++;
|
|
1170
|
+
else if (body[i] === "}")
|
|
1171
|
+
depth--;
|
|
1172
|
+
i++;
|
|
1173
|
+
}
|
|
1174
|
+
// Advance regex past the closing brace so we don't match sub-blocks
|
|
1175
|
+
re.lastIndex = i;
|
|
1176
|
+
if (depth === 0) {
|
|
1177
|
+
const innerBody = body.slice(startIdx, i - 1);
|
|
1178
|
+
const fields = parseFields(innerBody);
|
|
1179
|
+
// Parse models list
|
|
1180
|
+
const models = parseMultilineList(innerBody, "models");
|
|
1181
|
+
// Collect known fields
|
|
1182
|
+
const knownKeys = new Set(["api-key", "base-url", "models", "default"]);
|
|
1183
|
+
const extra = {};
|
|
1184
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
1185
|
+
if (!knownKeys.has(k)) {
|
|
1186
|
+
if (v === "true")
|
|
1187
|
+
extra[k] = true;
|
|
1188
|
+
else if (v === "false")
|
|
1189
|
+
extra[k] = false;
|
|
1190
|
+
else if (/^\d+$/.test(v))
|
|
1191
|
+
extra[k] = parseInt(v, 10);
|
|
1192
|
+
else
|
|
1193
|
+
extra[k] = unquote(v);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const provider = {
|
|
1197
|
+
name,
|
|
1198
|
+
models,
|
|
1199
|
+
extra,
|
|
1200
|
+
};
|
|
1201
|
+
if (fields["api-key"])
|
|
1202
|
+
provider.apiKey = unquote(fields["api-key"]);
|
|
1203
|
+
if (fields["base-url"])
|
|
1204
|
+
provider.baseUrl = unquote(fields["base-url"]);
|
|
1205
|
+
if (fields["default"])
|
|
1206
|
+
provider.default = fields["default"] === "true";
|
|
1207
|
+
// Parse auth sub-block
|
|
1208
|
+
const authBlock = extractBlock(innerBody, "auth");
|
|
1209
|
+
if (authBlock) {
|
|
1210
|
+
const aFields = parseFields(authBlock.body);
|
|
1211
|
+
const auth = {
|
|
1212
|
+
type: aFields.type ?? "api-key",
|
|
1213
|
+
};
|
|
1214
|
+
if (aFields.issuer)
|
|
1215
|
+
auth.issuer = unquote(aFields.issuer);
|
|
1216
|
+
if (aFields.audience)
|
|
1217
|
+
auth.audience = unquote(aFields.audience);
|
|
1218
|
+
if (aFields["token-url"])
|
|
1219
|
+
auth.tokenUrl = unquote(aFields["token-url"]);
|
|
1220
|
+
if (aFields["client-id"])
|
|
1221
|
+
auth.clientId = unquote(aFields["client-id"]);
|
|
1222
|
+
const scopes = parseMultilineList(authBlock.body, "scopes");
|
|
1223
|
+
if (scopes.length)
|
|
1224
|
+
auth.scopes = scopes;
|
|
1225
|
+
provider.auth = auth;
|
|
1226
|
+
}
|
|
1227
|
+
providers.push(provider);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return providers;
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Parse the `schedule { ... }` block.
|
|
1234
|
+
*
|
|
1235
|
+
* Extracts `job <id> { ... }` sub-blocks within the schedule section.
|
|
1236
|
+
*/
|
|
1237
|
+
export function parseSchedule(body) {
|
|
1238
|
+
const jobs = [];
|
|
1239
|
+
const jobBlocks = extractAllBlocks(body, "job");
|
|
1240
|
+
for (const block of jobBlocks) {
|
|
1241
|
+
if (!block.id)
|
|
1242
|
+
continue;
|
|
1243
|
+
const fields = parseFields(block.body);
|
|
1244
|
+
const job = {
|
|
1245
|
+
id: block.id,
|
|
1246
|
+
enabled: true,
|
|
1247
|
+
};
|
|
1248
|
+
if (fields.cron)
|
|
1249
|
+
job.cron = unquote(fields.cron);
|
|
1250
|
+
if (fields.every)
|
|
1251
|
+
job.every = unquote(fields.every);
|
|
1252
|
+
if (fields.agent)
|
|
1253
|
+
job.agent = fields.agent;
|
|
1254
|
+
if (fields.action)
|
|
1255
|
+
job.action = fields.action;
|
|
1256
|
+
if (fields.enabled)
|
|
1257
|
+
job.enabled = fields.enabled !== "false";
|
|
1258
|
+
jobs.push(job);
|
|
1259
|
+
}
|
|
1260
|
+
return jobs;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Parse the `interfaces { ... }` block.
|
|
1264
|
+
*
|
|
1265
|
+
* Each named sub-block declares an interface with a `type` field and arbitrary
|
|
1266
|
+
* configuration. Follows the same named-sub-block pattern as {@link parseMcpServers}.
|
|
1267
|
+
*/
|
|
1268
|
+
export function parseInterfaces(body) {
|
|
1269
|
+
const interfaces = [];
|
|
1270
|
+
const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
|
|
1271
|
+
let m;
|
|
1272
|
+
while ((m = re.exec(body)) !== null) {
|
|
1273
|
+
const name = m[1];
|
|
1274
|
+
const startIdx = m.index + m[0].length;
|
|
1275
|
+
let depth = 1;
|
|
1276
|
+
let i = startIdx;
|
|
1277
|
+
while (i < body.length && depth > 0) {
|
|
1278
|
+
if (body[i] === "{")
|
|
1279
|
+
depth++;
|
|
1280
|
+
else if (body[i] === "}")
|
|
1281
|
+
depth--;
|
|
1282
|
+
i++;
|
|
1283
|
+
}
|
|
1284
|
+
if (depth === 0) {
|
|
1285
|
+
const innerBody = body.slice(startIdx, i - 1);
|
|
1286
|
+
const fields = parseFields(innerBody);
|
|
1287
|
+
const config = {};
|
|
1288
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
1289
|
+
if (k === "type")
|
|
1290
|
+
continue;
|
|
1291
|
+
if (v === "true")
|
|
1292
|
+
config[k] = true;
|
|
1293
|
+
else if (v === "false")
|
|
1294
|
+
config[k] = false;
|
|
1295
|
+
else if (/^\d+$/.test(v))
|
|
1296
|
+
config[k] = parseInt(v, 10);
|
|
1297
|
+
else
|
|
1298
|
+
config[k] = unquote(v);
|
|
1299
|
+
}
|
|
1300
|
+
const iface = {
|
|
1301
|
+
id: name,
|
|
1302
|
+
config,
|
|
1303
|
+
};
|
|
1304
|
+
if (fields.type)
|
|
1305
|
+
iface.type = fields.type;
|
|
1306
|
+
interfaces.push(iface);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return interfaces;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Parse the top-level `tools { ... }` block containing `tool <id> { ... }` sub-blocks.
|
|
1313
|
+
*/
|
|
1314
|
+
export function parseToolsBlock(topBody) {
|
|
1315
|
+
const toolsBlock = extractBlock(topBody, "tools");
|
|
1316
|
+
const toolDefs = [];
|
|
1317
|
+
if (!toolsBlock)
|
|
1318
|
+
return toolDefs;
|
|
1319
|
+
const toolBlocks = extractAllBlocks(toolsBlock.body, "tool");
|
|
1320
|
+
for (const block of toolBlocks) {
|
|
1321
|
+
if (!block.id)
|
|
1322
|
+
continue;
|
|
1323
|
+
const tFields = parseFields(block.body);
|
|
1324
|
+
const td = {
|
|
1325
|
+
id: block.id,
|
|
1326
|
+
script: tFields.script ? unquote(tFields.script) : "",
|
|
1327
|
+
description: tFields.description ? unquote(tFields.description) : "",
|
|
1328
|
+
};
|
|
1329
|
+
const args = parseMultilineList(block.body, "args");
|
|
1330
|
+
if (args.length)
|
|
1331
|
+
td.args = args;
|
|
1332
|
+
if (tFields.lang)
|
|
1333
|
+
td.lang = tFields.lang;
|
|
1334
|
+
toolDefs.push(td);
|
|
1335
|
+
}
|
|
1336
|
+
return toolDefs;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Parse the `defaults { ... }` block.
|
|
1340
|
+
*
|
|
1341
|
+
* Extracts topology-level default values for sampling parameters and
|
|
1342
|
+
* shared agent configuration.
|
|
1343
|
+
*/
|
|
1344
|
+
export function parseDefaults(body) {
|
|
1345
|
+
const fields = parseFields(body);
|
|
1346
|
+
const defaults = {};
|
|
1347
|
+
let hasFields = false;
|
|
1348
|
+
if (fields.temperature) {
|
|
1349
|
+
defaults.temperature = parseFloat(fields.temperature);
|
|
1350
|
+
hasFields = true;
|
|
1351
|
+
}
|
|
1352
|
+
if (fields["max-tokens"]) {
|
|
1353
|
+
defaults.maxTokens = parseInt(fields["max-tokens"], 10);
|
|
1354
|
+
hasFields = true;
|
|
1355
|
+
}
|
|
1356
|
+
if (fields["top-p"]) {
|
|
1357
|
+
defaults.topP = parseFloat(fields["top-p"]);
|
|
1358
|
+
hasFields = true;
|
|
1359
|
+
}
|
|
1360
|
+
if (fields["top-k"]) {
|
|
1361
|
+
defaults.topK = parseInt(fields["top-k"], 10);
|
|
1362
|
+
hasFields = true;
|
|
1363
|
+
}
|
|
1364
|
+
const stop = parseMultilineList(body, "stop");
|
|
1365
|
+
if (stop.length) {
|
|
1366
|
+
defaults.stop = stop;
|
|
1367
|
+
hasFields = true;
|
|
1368
|
+
}
|
|
1369
|
+
if (fields.seed) {
|
|
1370
|
+
defaults.seed = parseInt(fields.seed, 10);
|
|
1371
|
+
hasFields = true;
|
|
1372
|
+
}
|
|
1373
|
+
if (fields.thinking) {
|
|
1374
|
+
defaults.thinking = fields.thinking;
|
|
1375
|
+
hasFields = true;
|
|
1376
|
+
}
|
|
1377
|
+
if (fields["thinking-budget"]) {
|
|
1378
|
+
defaults.thinkingBudget = parseInt(fields["thinking-budget"], 10);
|
|
1379
|
+
hasFields = true;
|
|
1380
|
+
}
|
|
1381
|
+
if (fields["output-format"]) {
|
|
1382
|
+
defaults.outputFormat = fields["output-format"];
|
|
1383
|
+
hasFields = true;
|
|
1384
|
+
}
|
|
1385
|
+
if (fields.timeout) {
|
|
1386
|
+
defaults.timeout = fields.timeout;
|
|
1387
|
+
hasFields = true;
|
|
1388
|
+
}
|
|
1389
|
+
if (fields["log-level"]) {
|
|
1390
|
+
defaults.logLevel = fields["log-level"];
|
|
1391
|
+
hasFields = true;
|
|
1392
|
+
}
|
|
1393
|
+
return hasFields ? defaults : null;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Parse the `observability { ... }` block.
|
|
1397
|
+
*
|
|
1398
|
+
* Extracts simple KV fields plus `capture { ... }` and `spans { ... }`
|
|
1399
|
+
* sub-blocks. Returns an {@link ObservabilityDef} with defaults applied
|
|
1400
|
+
* for any omitted fields.
|
|
1401
|
+
*/
|
|
1402
|
+
export function parseObservability(body) {
|
|
1403
|
+
const fields = parseFields(body);
|
|
1404
|
+
// Parse simple KV fields with defaults
|
|
1405
|
+
const enabled = fields.enabled !== undefined ? fields.enabled === "true" : true;
|
|
1406
|
+
const level = fields.level ?? "info";
|
|
1407
|
+
const exporter = fields.exporter ?? "otlp";
|
|
1408
|
+
const endpoint = fields.endpoint ? unquote(fields.endpoint) : undefined;
|
|
1409
|
+
const service = fields.service ? unquote(fields.service) : undefined;
|
|
1410
|
+
const sampleRate = fields["sample-rate"] !== undefined
|
|
1411
|
+
? parseFloat(fields["sample-rate"])
|
|
1412
|
+
: 1.0;
|
|
1413
|
+
// Parse capture sub-block with defaults (all false)
|
|
1414
|
+
const captureBlock = extractBlock(body, "capture");
|
|
1415
|
+
const captureFields = captureBlock ? parseFields(captureBlock.body) : {};
|
|
1416
|
+
const capture = {
|
|
1417
|
+
prompts: captureFields.prompts === "true",
|
|
1418
|
+
completions: captureFields.completions === "true",
|
|
1419
|
+
toolArgs: (captureFields["tool-args"] ?? captureFields.toolArgs) === "true",
|
|
1420
|
+
toolResults: (captureFields["tool-results"] ?? captureFields.toolResults) === "true",
|
|
1421
|
+
};
|
|
1422
|
+
// Parse spans sub-block with defaults (agents/tools/gates: true, memory: false)
|
|
1423
|
+
const spansBlock = extractBlock(body, "spans");
|
|
1424
|
+
const spansFields = spansBlock ? parseFields(spansBlock.body) : {};
|
|
1425
|
+
const spans = {
|
|
1426
|
+
agents: spansFields.agents !== undefined ? spansFields.agents === "true" : true,
|
|
1427
|
+
tools: spansFields.tools !== undefined ? spansFields.tools === "true" : true,
|
|
1428
|
+
gates: spansFields.gates !== undefined ? spansFields.gates === "true" : true,
|
|
1429
|
+
memory: spansFields.memory !== undefined ? spansFields.memory === "true" : false,
|
|
1430
|
+
};
|
|
1431
|
+
const def = {
|
|
1432
|
+
enabled,
|
|
1433
|
+
level,
|
|
1434
|
+
exporter,
|
|
1435
|
+
sampleRate,
|
|
1436
|
+
capture,
|
|
1437
|
+
spans,
|
|
1438
|
+
};
|
|
1439
|
+
if (endpoint !== undefined)
|
|
1440
|
+
def.endpoint = endpoint;
|
|
1441
|
+
if (service !== undefined)
|
|
1442
|
+
def.service = service;
|
|
1443
|
+
return def;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Parse the `checkpoint { ... }` block.
|
|
1447
|
+
*
|
|
1448
|
+
* Extracts KV fields plus an optional `replay { ... }` sub-block.
|
|
1449
|
+
* Returns a {@link CheckpointDef} with parsed connection supporting
|
|
1450
|
+
* the `secret "uri"` modifier.
|
|
1451
|
+
*/
|
|
1452
|
+
export function parseCheckpoint(body) {
|
|
1453
|
+
const fields = parseFields(body);
|
|
1454
|
+
const def = {
|
|
1455
|
+
backend: fields.backend ?? "memory",
|
|
1456
|
+
strategy: fields.strategy ?? "every-node",
|
|
1457
|
+
};
|
|
1458
|
+
// Connection: may use `secret "uri"` modifier
|
|
1459
|
+
if (fields.connection) {
|
|
1460
|
+
const rawConn = fields.connection.trim();
|
|
1461
|
+
if (rawConn.startsWith("secret ")) {
|
|
1462
|
+
const uri = unquote(rawConn.slice("secret ".length).trim());
|
|
1463
|
+
def.connection = uri;
|
|
1464
|
+
}
|
|
1465
|
+
else {
|
|
1466
|
+
def.connection = unquote(rawConn);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
if (fields.ttl)
|
|
1470
|
+
def.ttl = fields.ttl;
|
|
1471
|
+
// Parse replay sub-block
|
|
1472
|
+
const replayBlock = extractBlock(body, "replay");
|
|
1473
|
+
if (replayBlock) {
|
|
1474
|
+
const rFields = parseFields(replayBlock.body);
|
|
1475
|
+
const replay = {
|
|
1476
|
+
enabled: rFields.enabled === "true",
|
|
1477
|
+
};
|
|
1478
|
+
if (rFields["max-history"]) {
|
|
1479
|
+
replay.maxHistory = parseInt(rFields["max-history"], 10);
|
|
1480
|
+
}
|
|
1481
|
+
if (rFields.branch) {
|
|
1482
|
+
replay.branch = rFields.branch === "true";
|
|
1483
|
+
}
|
|
1484
|
+
def.replay = replay;
|
|
1485
|
+
}
|
|
1486
|
+
return def;
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Parse the top-level `artifacts { artifact <id> { ... } ... }` block.
|
|
1490
|
+
*
|
|
1491
|
+
* Returns an array of artifact definitions with their dependencies.
|
|
1492
|
+
*/
|
|
1493
|
+
export function parseArtifacts(topBody) {
|
|
1494
|
+
const artifactsBlock = extractBlock(topBody, "artifacts");
|
|
1495
|
+
if (!artifactsBlock)
|
|
1496
|
+
return [];
|
|
1497
|
+
const artifactBlocks = extractAllBlocks(artifactsBlock.body, "artifact");
|
|
1498
|
+
const artifacts = [];
|
|
1499
|
+
for (const block of artifactBlocks) {
|
|
1500
|
+
if (!block.id)
|
|
1501
|
+
continue;
|
|
1502
|
+
const fields = parseFields(block.body);
|
|
1503
|
+
const artifact = {
|
|
1504
|
+
id: block.id,
|
|
1505
|
+
type: fields.type ?? "unknown",
|
|
1506
|
+
};
|
|
1507
|
+
if (fields.path)
|
|
1508
|
+
artifact.path = unquote(fields.path);
|
|
1509
|
+
if (fields.retention)
|
|
1510
|
+
artifact.retention = fields.retention;
|
|
1511
|
+
const dependsOn = parseMultilineList(block.body, "depends-on");
|
|
1512
|
+
if (dependsOn.length)
|
|
1513
|
+
artifact.dependsOn = dependsOn;
|
|
1514
|
+
artifacts.push(artifact);
|
|
1515
|
+
}
|
|
1516
|
+
return artifacts;
|
|
1517
|
+
}
|
|
1518
|
+
// ---------------------------------------------------------------------------
|
|
1519
|
+
// Main parser
|
|
1520
|
+
// ---------------------------------------------------------------------------
|
|
1521
|
+
/**
|
|
1522
|
+
* Parse an AgenTopology `.at` source string into a complete {@link TopologyAST}.
|
|
1523
|
+
*
|
|
1524
|
+
* @param source - The raw `.at` file content.
|
|
1525
|
+
* @returns The fully-parsed AST.
|
|
1526
|
+
* @throws If the topology header or body cannot be extracted.
|
|
1527
|
+
*/
|
|
1528
|
+
/**
|
|
1529
|
+
* Build a source map that records the line number (1-based) where each named
|
|
1530
|
+
* block starts in the raw source. Scans for `agent <id> {`, `gate <id> {`,
|
|
1531
|
+
* `action <id> {`, and `orchestrator {` patterns.
|
|
1532
|
+
*/
|
|
1533
|
+
function buildSourceMap(rawSource) {
|
|
1534
|
+
const sourceMap = {};
|
|
1535
|
+
const lines = rawSource.split("\n");
|
|
1536
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1537
|
+
const line = lines[i];
|
|
1538
|
+
// Match: agent <id> {, action <id> {, gate <id> {, human <id> {
|
|
1539
|
+
const namedMatch = line.match(/^\s*(?:agent|action|gate|human|group)\s+([a-zA-Z][a-zA-Z0-9_-]*)\s*(?:\{|[^{]*\{)/);
|
|
1540
|
+
if (namedMatch) {
|
|
1541
|
+
sourceMap[namedMatch[1]] = i + 1; // 1-based line number
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
// Match: orchestrator {
|
|
1545
|
+
const orchMatch = line.match(/^\s*orchestrator\s*(?:\{|[^{]*\{)/);
|
|
1546
|
+
if (orchMatch) {
|
|
1547
|
+
sourceMap["orchestrator"] = i + 1;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return sourceMap;
|
|
1551
|
+
}
|
|
1552
|
+
// ---------------------------------------------------------------------------
|
|
1553
|
+
// Composition parsing: params, interface, import, include, fragment
|
|
1554
|
+
// ---------------------------------------------------------------------------
|
|
1555
|
+
/**
|
|
1556
|
+
* Parse a `params { ... }` block into typed parameter definitions.
|
|
1557
|
+
*
|
|
1558
|
+
* Each line has the form `name: type` or `name: type = default`.
|
|
1559
|
+
* If a default is provided, the parameter is optional; otherwise required.
|
|
1560
|
+
*/
|
|
1561
|
+
export function parseParams(body) {
|
|
1562
|
+
const params = [];
|
|
1563
|
+
const VALID_TYPES = new Set(["string", "number", "boolean"]);
|
|
1564
|
+
for (const line of body.split("\n")) {
|
|
1565
|
+
const trimmed = line.trim();
|
|
1566
|
+
if (!trimmed)
|
|
1567
|
+
continue;
|
|
1568
|
+
// Match: name: type = default OR name: type
|
|
1569
|
+
const m = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(string|number|boolean)(?:\s*=\s*(.+))?$/);
|
|
1570
|
+
if (!m)
|
|
1571
|
+
continue;
|
|
1572
|
+
const [, name, type, rawDefault] = m;
|
|
1573
|
+
const paramType = type;
|
|
1574
|
+
let defaultValue;
|
|
1575
|
+
let required = true;
|
|
1576
|
+
if (rawDefault !== undefined) {
|
|
1577
|
+
required = false;
|
|
1578
|
+
const dv = rawDefault.trim();
|
|
1579
|
+
if (paramType === "number") {
|
|
1580
|
+
defaultValue = parseFloat(dv);
|
|
1581
|
+
}
|
|
1582
|
+
else if (paramType === "boolean") {
|
|
1583
|
+
defaultValue = dv === "true";
|
|
1584
|
+
}
|
|
1585
|
+
else {
|
|
1586
|
+
// string — strip quotes if present
|
|
1587
|
+
defaultValue = unquote(dv);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
params.push({ name, type: paramType, required, ...(defaultValue !== undefined ? { default: defaultValue } : {}) });
|
|
1591
|
+
}
|
|
1592
|
+
return params;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Parse an `interface { entry: <id> exit: <id> }` block body.
|
|
1596
|
+
*/
|
|
1597
|
+
export function parseInterfaceEndpoints(body) {
|
|
1598
|
+
const fields = parseFields(body);
|
|
1599
|
+
const entry = fields["entry"];
|
|
1600
|
+
const exit = fields["exit"];
|
|
1601
|
+
if (!entry || !exit)
|
|
1602
|
+
return null;
|
|
1603
|
+
return { entry: unquote(entry), exit: unquote(exit) };
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Extract and parse the `interface { ... }` block from a topology body,
|
|
1607
|
+
* carefully avoiding collision with `interfaces { ... }`.
|
|
1608
|
+
*/
|
|
1609
|
+
function parseInterfaceBlock(topBody) {
|
|
1610
|
+
// Match "interface" followed by whitespace+"{" but NOT "interfaces"
|
|
1611
|
+
const re = /(?:^|\n)\s*interface\s*\{/m;
|
|
1612
|
+
const m = re.exec(topBody);
|
|
1613
|
+
if (!m)
|
|
1614
|
+
return null;
|
|
1615
|
+
// Make sure we did not match "interfaces" — check what precedes the `{`
|
|
1616
|
+
const matchText = m[0];
|
|
1617
|
+
if (/interfaces/.test(matchText))
|
|
1618
|
+
return null;
|
|
1619
|
+
// Find the opening brace and count braces to extract the body
|
|
1620
|
+
const braceIdx = m.index + m[0].length;
|
|
1621
|
+
let depth = 1;
|
|
1622
|
+
let i = braceIdx;
|
|
1623
|
+
while (i < topBody.length && depth > 0) {
|
|
1624
|
+
if (topBody[i] === "{")
|
|
1625
|
+
depth++;
|
|
1626
|
+
else if (topBody[i] === "}")
|
|
1627
|
+
depth--;
|
|
1628
|
+
i++;
|
|
1629
|
+
}
|
|
1630
|
+
if (depth !== 0)
|
|
1631
|
+
return null;
|
|
1632
|
+
const body = topBody.slice(braceIdx, i - 1);
|
|
1633
|
+
return parseInterfaceEndpoints(body);
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Parse import statements from a topology body.
|
|
1637
|
+
*
|
|
1638
|
+
* Syntax:
|
|
1639
|
+
* import "./path.at" as alias
|
|
1640
|
+
* import "./path.at" as alias with { key: value ... }
|
|
1641
|
+
*
|
|
1642
|
+
* Import statements are parsed from the raw body text since they are
|
|
1643
|
+
* not brace-delimited blocks in the usual sense.
|
|
1644
|
+
*/
|
|
1645
|
+
export function parseImports(body) {
|
|
1646
|
+
const imports = [];
|
|
1647
|
+
// Regex: import "source" as alias (captures everything after for optional with block)
|
|
1648
|
+
const importRe = /(?:^|\n)\s*import\s+"([^"]+)"\s+as\s+([a-zA-Z][a-zA-Z0-9_-]*)/g;
|
|
1649
|
+
let m;
|
|
1650
|
+
while ((m = importRe.exec(body)) !== null) {
|
|
1651
|
+
const source = m[1];
|
|
1652
|
+
const alias = m[2];
|
|
1653
|
+
const params = {};
|
|
1654
|
+
// Check for `with { ... }` block following the import
|
|
1655
|
+
const afterImport = body.slice(m.index + m[0].length);
|
|
1656
|
+
const withMatch = afterImport.match(/^\s*with\s*\{([^}]*)\}/);
|
|
1657
|
+
if (withMatch) {
|
|
1658
|
+
const withBody = withMatch[1];
|
|
1659
|
+
for (const line of withBody.split("\n")) {
|
|
1660
|
+
const kv = parseKV(line);
|
|
1661
|
+
if (!kv)
|
|
1662
|
+
continue;
|
|
1663
|
+
const [key, rawVal] = kv;
|
|
1664
|
+
const val = unquote(rawVal);
|
|
1665
|
+
// Try number
|
|
1666
|
+
if (/^-?\d+(\.\d+)?$/.test(val)) {
|
|
1667
|
+
params[key] = parseFloat(val);
|
|
1668
|
+
}
|
|
1669
|
+
else if (val === "true" || val === "false") {
|
|
1670
|
+
params[key] = val === "true";
|
|
1671
|
+
}
|
|
1672
|
+
else {
|
|
1673
|
+
params[key] = val;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
const importDef = { source, alias, params };
|
|
1678
|
+
// Detect registry imports (not starting with ./ or ../)
|
|
1679
|
+
const isRegistry = !source.startsWith("./") && !source.startsWith("../");
|
|
1680
|
+
if (isRegistry) {
|
|
1681
|
+
importDef.registry = true;
|
|
1682
|
+
// Parse package@version format
|
|
1683
|
+
const atIdx = source.lastIndexOf("@");
|
|
1684
|
+
if (atIdx > 0) {
|
|
1685
|
+
importDef.registryPackage = source.slice(0, atIdx);
|
|
1686
|
+
importDef.registryVersion = source.slice(atIdx + 1);
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
importDef.registryPackage = source;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
// Check for sha256 after the with block (or after alias if no with block)
|
|
1693
|
+
const afterWithOrAlias = withMatch
|
|
1694
|
+
? afterImport.slice(withMatch[0].length)
|
|
1695
|
+
: afterImport;
|
|
1696
|
+
const sha256Match = afterWithOrAlias.match(/^\s*sha256\s*:\s*"([^"]+)"/);
|
|
1697
|
+
if (sha256Match) {
|
|
1698
|
+
importDef.sha256 = sha256Match[1];
|
|
1699
|
+
}
|
|
1700
|
+
imports.push(importDef);
|
|
1701
|
+
}
|
|
1702
|
+
return imports;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Parse include statements from a topology body.
|
|
1706
|
+
*
|
|
1707
|
+
* Syntax: include "./path.at"
|
|
1708
|
+
*/
|
|
1709
|
+
export function parseIncludes(body) {
|
|
1710
|
+
const includes = [];
|
|
1711
|
+
const re = /(?:^|\n)\s*include\s+"([^"]+)"/g;
|
|
1712
|
+
let m;
|
|
1713
|
+
while ((m = re.exec(body)) !== null) {
|
|
1714
|
+
includes.push({ source: m[1] });
|
|
1715
|
+
}
|
|
1716
|
+
return includes;
|
|
1717
|
+
}
|
|
1718
|
+
export function parse(source) {
|
|
1719
|
+
const src = stripComments(source);
|
|
1720
|
+
// --- Source map (line numbers for nodes) ---
|
|
1721
|
+
const sourceMap = buildSourceMap(source);
|
|
1722
|
+
// --- Detect fragment vs topology root ---
|
|
1723
|
+
const isFragment = /(?:^|\n)\s*fragment\s+\S+\s*\{/.test(source);
|
|
1724
|
+
// --- Topology header ---
|
|
1725
|
+
let header;
|
|
1726
|
+
if (isFragment) {
|
|
1727
|
+
// Fragment header: fragment <name> { ... }
|
|
1728
|
+
const fm = source.match(/fragment\s+(\S+)\s*\{/);
|
|
1729
|
+
if (!fm)
|
|
1730
|
+
throw new Error("Could not find fragment header");
|
|
1731
|
+
header = { name: fm[1], patterns: [] };
|
|
1732
|
+
}
|
|
1733
|
+
else {
|
|
1734
|
+
// Use raw source to preserve the topology line (comments already on other lines)
|
|
1735
|
+
header = parseTopologyHeader(source);
|
|
1736
|
+
}
|
|
1737
|
+
// --- Extract the full topology/fragment body ---
|
|
1738
|
+
const topoBlock = extractBlock(source, isFragment ? "fragment" : "topology");
|
|
1739
|
+
if (!topoBlock) {
|
|
1740
|
+
throw new Error(isFragment ? "Could not extract fragment body" : "Could not extract topology body");
|
|
1741
|
+
}
|
|
1742
|
+
const topBody = stripComments(topoBlock.body);
|
|
1743
|
+
// --- Meta ---
|
|
1744
|
+
const metaBlock = extractBlock(topBody, "meta");
|
|
1745
|
+
const metaFields = metaBlock ? parseMeta(metaBlock.body) : {};
|
|
1746
|
+
const topology = {
|
|
1747
|
+
name: header.name,
|
|
1748
|
+
version: metaFields.version ?? "0.0.0",
|
|
1749
|
+
description: metaFields.description ?? "",
|
|
1750
|
+
patterns: header.patterns,
|
|
1751
|
+
...(metaFields.foundations ? { foundations: metaFields.foundations } : {}),
|
|
1752
|
+
...(metaFields.advanced ? { advanced: metaFields.advanced } : {}),
|
|
1753
|
+
...(metaFields.domain ? { domain: metaFields.domain } : {}),
|
|
1754
|
+
...(metaFields.timeout ? { timeout: metaFields.timeout } : {}),
|
|
1755
|
+
...(metaFields.errorHandler ? { errorHandler: metaFields.errorHandler } : {}),
|
|
1756
|
+
...(metaFields.durable !== undefined ? { durable: metaFields.durable } : {}),
|
|
1757
|
+
};
|
|
1758
|
+
// --- Roles ---
|
|
1759
|
+
const rolesBlock = extractBlock(topBody, "roles");
|
|
1760
|
+
const roles = rolesBlock ? parseRoles(rolesBlock.body) : {};
|
|
1761
|
+
// --- Nodes ---
|
|
1762
|
+
const nodes = [];
|
|
1763
|
+
// Orchestrator
|
|
1764
|
+
const orchBlock = extractBlock(topBody, "orchestrator");
|
|
1765
|
+
if (orchBlock) {
|
|
1766
|
+
nodes.push(parseOrchestrator(orchBlock.body));
|
|
1767
|
+
}
|
|
1768
|
+
// Actions
|
|
1769
|
+
const actionBlocks = extractAllBlocks(topBody, "action");
|
|
1770
|
+
for (const block of actionBlocks) {
|
|
1771
|
+
if (block.id) {
|
|
1772
|
+
nodes.push(parseAction(block.id, block.body));
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
// Agents
|
|
1776
|
+
const agentBlocks = extractAllBlocks(topBody, "agent");
|
|
1777
|
+
for (const block of agentBlocks) {
|
|
1778
|
+
if (block.id) {
|
|
1779
|
+
nodes.push(parseAgent(block.id, block.body, roles));
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
// Gates
|
|
1783
|
+
const gatesBlock = extractBlock(topBody, "gates");
|
|
1784
|
+
if (gatesBlock) {
|
|
1785
|
+
const gateBlocks = extractAllBlocks(gatesBlock.body, "gate");
|
|
1786
|
+
for (const block of gateBlocks) {
|
|
1787
|
+
if (block.id) {
|
|
1788
|
+
nodes.push(parseGate(block.id, block.body));
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
// Human nodes
|
|
1793
|
+
const humanBlocks = extractAllBlocks(topBody, "human");
|
|
1794
|
+
for (const block of humanBlocks) {
|
|
1795
|
+
if (block.id) {
|
|
1796
|
+
nodes.push(parseHuman(block.id, block.body));
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
// Group chat nodes
|
|
1800
|
+
const groupBlocks = extractAllBlocks(topBody, "group");
|
|
1801
|
+
for (const block of groupBlocks) {
|
|
1802
|
+
if (block.id) {
|
|
1803
|
+
nodes.push(parseGroup(block.id, block.body));
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
// --- Skills ---
|
|
1807
|
+
const skillDefs = parseSkillBlocks(topBody);
|
|
1808
|
+
// --- Tools ---
|
|
1809
|
+
const toolDefs = parseToolsBlock(topBody);
|
|
1810
|
+
// --- Edges (flow) ---
|
|
1811
|
+
const flowBlock = extractBlock(topBody, "flow");
|
|
1812
|
+
const edgeAttributeErrors = [];
|
|
1813
|
+
const edges = flowBlock ? parseFlow(flowBlock.body, edgeAttributeErrors) : [];
|
|
1814
|
+
// --- Depth ---
|
|
1815
|
+
const depthBlock = extractBlock(topBody, "depth");
|
|
1816
|
+
const depth = depthBlock
|
|
1817
|
+
? parseDepth(depthBlock.body)
|
|
1818
|
+
: { factors: [], levels: [] };
|
|
1819
|
+
// --- Memory ---
|
|
1820
|
+
const memoryBlock = extractBlock(topBody, "memory");
|
|
1821
|
+
const unknownMemorySubBlockWarnings = [];
|
|
1822
|
+
const memory = memoryBlock ? parseMemory(memoryBlock.body, unknownMemorySubBlockWarnings) : {};
|
|
1823
|
+
// --- Batch ---
|
|
1824
|
+
const batchBlock = extractBlock(topBody, "batch");
|
|
1825
|
+
const batch = batchBlock ? parseBatch(batchBlock.body) : {};
|
|
1826
|
+
// --- Environments ---
|
|
1827
|
+
const envsBlock = extractBlock(topBody, "environments");
|
|
1828
|
+
const environments = envsBlock
|
|
1829
|
+
? parseEnvironments(envsBlock.body)
|
|
1830
|
+
: {};
|
|
1831
|
+
// --- Triggers ---
|
|
1832
|
+
const triggersBlock = extractBlock(topBody, "triggers");
|
|
1833
|
+
const triggers = triggersBlock
|
|
1834
|
+
? parseTriggers(triggersBlock.body)
|
|
1835
|
+
: [];
|
|
1836
|
+
// --- Hooks (global) ---
|
|
1837
|
+
// Collect per-agent hook names to avoid double-counting them as global.
|
|
1838
|
+
const perAgentHookNames = new Set();
|
|
1839
|
+
for (const n of nodes) {
|
|
1840
|
+
if (n.type === "agent" && n.hooks) {
|
|
1841
|
+
for (const h of n.hooks) {
|
|
1842
|
+
perAgentHookNames.add(h.name);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const allHooksBlocks = extractAllBlocks(topBody, "hooks");
|
|
1847
|
+
let hooks = [];
|
|
1848
|
+
for (const hb of allHooksBlocks) {
|
|
1849
|
+
const parsed = parseHooks(hb.body);
|
|
1850
|
+
const globalOnly = parsed.filter((h) => !perAgentHookNames.has(h.name));
|
|
1851
|
+
if (globalOnly.length > 0) {
|
|
1852
|
+
hooks = globalOnly;
|
|
1853
|
+
break;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (hooks.length === 0 && allHooksBlocks.length > 1) {
|
|
1857
|
+
const lastBlock = allHooksBlocks[allHooksBlocks.length - 1];
|
|
1858
|
+
hooks = parseHooks(lastBlock.body);
|
|
1859
|
+
}
|
|
1860
|
+
// --- Settings ---
|
|
1861
|
+
const settingsBlock = extractBlock(topBody, "settings");
|
|
1862
|
+
const settings = settingsBlock
|
|
1863
|
+
? parseSettings(settingsBlock.body)
|
|
1864
|
+
: {};
|
|
1865
|
+
// --- MCP Servers ---
|
|
1866
|
+
const mcpBlock = extractBlock(topBody, "mcp-servers");
|
|
1867
|
+
const mcpServers = mcpBlock ? parseMcpServers(mcpBlock.body) : {};
|
|
1868
|
+
// --- Metering ---
|
|
1869
|
+
const meteringBlock = extractBlock(topBody, "metering");
|
|
1870
|
+
const metering = meteringBlock
|
|
1871
|
+
? parseMetering(meteringBlock.body)
|
|
1872
|
+
: null;
|
|
1873
|
+
// --- Context ---
|
|
1874
|
+
const contextBlock = extractBlock(topBody, "context");
|
|
1875
|
+
const context = contextBlock ? parseContext(contextBlock.body) : {};
|
|
1876
|
+
// --- Env ---
|
|
1877
|
+
const envBlock = extractBlock(topBody, "env");
|
|
1878
|
+
const env = envBlock ? parseEnv(envBlock.body) : {};
|
|
1879
|
+
// --- Providers ---
|
|
1880
|
+
const providersBlock = extractBlock(topBody, "providers");
|
|
1881
|
+
const providers = providersBlock ? parseProviders(providersBlock.body) : [];
|
|
1882
|
+
// --- Schedule ---
|
|
1883
|
+
const scheduleBlock = extractBlock(topBody, "schedule");
|
|
1884
|
+
const schedules = scheduleBlock ? parseSchedule(scheduleBlock.body) : [];
|
|
1885
|
+
// --- Interfaces ---
|
|
1886
|
+
const interfacesBlock = extractBlock(topBody, "interfaces");
|
|
1887
|
+
const interfaces = interfacesBlock ? parseInterfaces(interfacesBlock.body) : [];
|
|
1888
|
+
// --- Defaults ---
|
|
1889
|
+
const defaultsBlock = extractBlock(topBody, "defaults");
|
|
1890
|
+
const defaults = defaultsBlock ? parseDefaults(defaultsBlock.body) : null;
|
|
1891
|
+
// --- Schemas ---
|
|
1892
|
+
const schemas = parseSchemas(topBody);
|
|
1893
|
+
// --- Observability ---
|
|
1894
|
+
const observabilityBlock = extractBlock(topBody, "observability");
|
|
1895
|
+
const observability = observabilityBlock ? parseObservability(observabilityBlock.body) : null;
|
|
1896
|
+
// --- Checkpoint ---
|
|
1897
|
+
const checkpointBlock = extractBlock(topBody, "checkpoint");
|
|
1898
|
+
const checkpoint = checkpointBlock ? parseCheckpoint(checkpointBlock.body) : null;
|
|
1899
|
+
// --- Artifacts ---
|
|
1900
|
+
const artifacts = parseArtifacts(topBody);
|
|
1901
|
+
// --- Top-level Extensions ---
|
|
1902
|
+
const topLevelExtensions = parseExtensionsBlock(topBody);
|
|
1903
|
+
// --- Params ---
|
|
1904
|
+
const paramsBlock = extractBlock(topBody, "params");
|
|
1905
|
+
const params = paramsBlock ? parseParams(paramsBlock.body) : [];
|
|
1906
|
+
// --- Interface (entry/exit endpoints) ---
|
|
1907
|
+
// Note: "interface" block is distinct from "interfaces" (external interface definitions).
|
|
1908
|
+
// We use a targeted regex to avoid matching "interfaces" when looking for "interface".
|
|
1909
|
+
const interfaceEndpoints = parseInterfaceBlock(topBody);
|
|
1910
|
+
// --- Imports ---
|
|
1911
|
+
const imports = parseImports(topBody);
|
|
1912
|
+
// --- Includes ---
|
|
1913
|
+
const includes = parseIncludes(topBody);
|
|
1914
|
+
// --- Duplicate section detection ---
|
|
1915
|
+
const singletonKeywords = [
|
|
1916
|
+
"meta", "orchestrator", "flow", "memory", "batch", "environments",
|
|
1917
|
+
"triggers", "settings", "mcp-servers", "metering", "context", "env",
|
|
1918
|
+
"providers", "schedule", "interfaces", "depth", "gates", "roles", "tools",
|
|
1919
|
+
"defaults", "schemas", "observability", "params", "interface",
|
|
1920
|
+
"checkpoint", "artifacts",
|
|
1921
|
+
];
|
|
1922
|
+
const duplicateSectionWarnings = [];
|
|
1923
|
+
for (const keyword of singletonKeywords) {
|
|
1924
|
+
const allBlocks = extractAllBlocks(topBody, keyword);
|
|
1925
|
+
if (allBlocks.length > 1) {
|
|
1926
|
+
duplicateSectionWarnings.push({
|
|
1927
|
+
rule: "V23",
|
|
1928
|
+
level: "warning",
|
|
1929
|
+
message: `Duplicate "${keyword}" block detected (found ${allBlocks.length}) — only the first occurrence is used`,
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
// --- Assemble AST ---
|
|
1934
|
+
const ast = {
|
|
1935
|
+
topology,
|
|
1936
|
+
nodes,
|
|
1937
|
+
edges,
|
|
1938
|
+
depth,
|
|
1939
|
+
memory,
|
|
1940
|
+
batch,
|
|
1941
|
+
environments,
|
|
1942
|
+
triggers,
|
|
1943
|
+
hooks,
|
|
1944
|
+
settings,
|
|
1945
|
+
mcpServers,
|
|
1946
|
+
metering,
|
|
1947
|
+
skills: skillDefs,
|
|
1948
|
+
toolDefs,
|
|
1949
|
+
roles,
|
|
1950
|
+
context,
|
|
1951
|
+
env,
|
|
1952
|
+
providers,
|
|
1953
|
+
schedules,
|
|
1954
|
+
interfaces,
|
|
1955
|
+
defaults,
|
|
1956
|
+
schemas,
|
|
1957
|
+
...(topLevelExtensions ? { extensions: topLevelExtensions } : {}),
|
|
1958
|
+
observability,
|
|
1959
|
+
params,
|
|
1960
|
+
interfaceEndpoints,
|
|
1961
|
+
imports,
|
|
1962
|
+
includes,
|
|
1963
|
+
checkpoint,
|
|
1964
|
+
artifacts,
|
|
1965
|
+
...(isFragment ? { isFragment: true } : {}),
|
|
1966
|
+
};
|
|
1967
|
+
// Attach V12 parse-time errors for the validator to consume.
|
|
1968
|
+
if (edgeAttributeErrors.length > 0) {
|
|
1969
|
+
ast._edgeAttributeErrors = edgeAttributeErrors;
|
|
1970
|
+
}
|
|
1971
|
+
// Attach V23 duplicate section warnings for the validator to consume.
|
|
1972
|
+
if (duplicateSectionWarnings.length > 0) {
|
|
1973
|
+
ast._duplicateSectionWarnings = duplicateSectionWarnings;
|
|
1974
|
+
}
|
|
1975
|
+
// Attach V24 unknown memory sub-block warnings for the validator to consume.
|
|
1976
|
+
if (unknownMemorySubBlockWarnings.length > 0) {
|
|
1977
|
+
ast._unknownMemorySubBlockWarnings = unknownMemorySubBlockWarnings;
|
|
1978
|
+
}
|
|
1979
|
+
// Attach source map for line number tracking in validation results.
|
|
1980
|
+
if (Object.keys(sourceMap).length > 0) {
|
|
1981
|
+
ast._sourceMap = sourceMap;
|
|
1982
|
+
}
|
|
1983
|
+
return ast;
|
|
1984
|
+
}
|
|
1985
|
+
//# sourceMappingURL=index.js.map
|