agentopology 0.2.5 → 0.4.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/README.md +121 -7
- package/dist/analyzer/__tests__/analyzer.test.js +36 -9
- package/dist/analyzer/__tests__/analyzer.test.js.map +1 -1
- package/dist/analyzer/__tests__/observability-gap.test.js +40 -10
- package/dist/analyzer/__tests__/observability-gap.test.js.map +1 -1
- package/dist/analyzer/index.d.ts +2 -1
- package/dist/analyzer/index.d.ts.map +1 -1
- package/dist/analyzer/index.js +1 -1
- package/dist/analyzer/index.js.map +1 -1
- package/dist/bindings/__tests__/bindings.test.js +20 -5
- package/dist/bindings/__tests__/bindings.test.js.map +1 -1
- package/dist/bindings/__tests__/claude-workflow.test.js +12 -3
- package/dist/bindings/__tests__/claude-workflow.test.js.map +1 -1
- package/dist/bindings/__tests__/defaults-agreement.test.d.ts +16 -0
- package/dist/bindings/__tests__/defaults-agreement.test.d.ts.map +1 -0
- package/dist/bindings/__tests__/defaults-agreement.test.js +80 -0
- package/dist/bindings/__tests__/defaults-agreement.test.js.map +1 -0
- package/dist/bindings/__tests__/observability.test.js +8 -2
- package/dist/bindings/__tests__/observability.test.js.map +1 -1
- package/dist/bindings/__tests__/stub-generation.test.js +8 -2
- package/dist/bindings/__tests__/stub-generation.test.js.map +1 -1
- package/dist/bindings/claude-code.d.ts.map +1 -1
- package/dist/bindings/claude-code.js +102 -3
- package/dist/bindings/claude-code.js.map +1 -1
- package/dist/bindings/claude-workflow.d.ts.map +1 -1
- package/dist/bindings/claude-workflow.js +2 -1
- package/dist/bindings/claude-workflow.js.map +1 -1
- package/dist/bindings/codex.d.ts.map +1 -1
- package/dist/bindings/codex.js +6 -2
- package/dist/bindings/codex.js.map +1 -1
- package/dist/bindings/gemini-cli.d.ts.map +1 -1
- package/dist/bindings/gemini-cli.js +2 -1
- package/dist/bindings/gemini-cli.js.map +1 -1
- package/dist/bindings/kiro.d.ts.map +1 -1
- package/dist/bindings/kiro.js +2 -1
- package/dist/bindings/kiro.js.map +1 -1
- package/dist/bindings/openclaw.d.ts.map +1 -1
- package/dist/bindings/openclaw.js +2 -1
- package/dist/bindings/openclaw.js.map +1 -1
- package/dist/cli/__tests__/cli.test.d.ts +12 -0
- package/dist/cli/__tests__/cli.test.d.ts.map +1 -0
- package/dist/cli/__tests__/cli.test.js +171 -0
- package/dist/cli/__tests__/cli.test.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +344 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/docs/content.d.ts.map +1 -1
- package/dist/docs/content.js +112 -0
- package/dist/docs/content.js.map +1 -1
- package/dist/exporters/ascii.d.ts +13 -0
- package/dist/exporters/ascii.d.ts.map +1 -0
- package/dist/exporters/ascii.js +26 -0
- package/dist/exporters/ascii.js.map +1 -0
- package/dist/exporters/brief.d.ts +14 -0
- package/dist/exporters/brief.d.ts.map +1 -0
- package/dist/exporters/brief.js +37 -0
- package/dist/exporters/brief.js.map +1 -0
- package/dist/exporters/index.d.ts +2 -0
- package/dist/exporters/index.d.ts.map +1 -1
- package/dist/exporters/index.js +6 -0
- package/dist/exporters/index.js.map +1 -1
- package/dist/exporters/markdown.d.ts.map +1 -1
- package/dist/exporters/markdown.js +2 -1
- package/dist/exporters/markdown.js.map +1 -1
- package/dist/exporters/mermaid.d.ts.map +1 -1
- package/dist/exporters/mermaid.js +2 -1
- package/dist/exporters/mermaid.js.map +1 -1
- package/dist/import/__tests__/import.test.js +8 -2
- package/dist/import/__tests__/import.test.js.map +1 -1
- package/dist/parser/__tests__/parser.test.js +1553 -207
- package/dist/parser/__tests__/parser.test.js.map +1 -1
- package/dist/parser/__tests__/spec-sync.test.d.ts +10 -0
- package/dist/parser/__tests__/spec-sync.test.d.ts.map +1 -0
- package/dist/parser/__tests__/spec-sync.test.js +60 -0
- package/dist/parser/__tests__/spec-sync.test.js.map +1 -0
- package/dist/parser/ast.d.ts +39 -0
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/index.d.ts +50 -6
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +241 -30
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/validator.d.ts +7 -1
- package/dist/parser/validator.d.ts.map +1 -1
- package/dist/parser/validator.js +143 -5
- package/dist/parser/validator.js.map +1 -1
- package/dist/plan/__tests__/brief.test.d.ts +2 -0
- package/dist/plan/__tests__/brief.test.d.ts.map +1 -0
- package/dist/plan/__tests__/brief.test.js +442 -0
- package/dist/plan/__tests__/brief.test.js.map +1 -0
- package/dist/plan/brief.d.ts +207 -0
- package/dist/plan/brief.d.ts.map +1 -0
- package/dist/plan/brief.js +342 -0
- package/dist/plan/brief.js.map +1 -0
- package/dist/plan/render.d.ts +20 -0
- package/dist/plan/render.d.ts.map +1 -0
- package/dist/plan/render.js +425 -0
- package/dist/plan/render.js.map +1 -0
- package/dist/render/__tests__/ascii.test.d.ts +2 -0
- package/dist/render/__tests__/ascii.test.d.ts.map +1 -0
- package/dist/render/__tests__/ascii.test.js +171 -0
- package/dist/render/__tests__/ascii.test.js.map +1 -0
- package/dist/render/ascii.d.ts +23 -0
- package/dist/render/ascii.d.ts.map +1 -0
- package/dist/render/ascii.js +178 -0
- package/dist/render/ascii.js.map +1 -0
- package/dist/resolve/__tests__/defaults.test.d.ts +2 -0
- package/dist/resolve/__tests__/defaults.test.d.ts.map +1 -0
- package/dist/resolve/__tests__/defaults.test.js +109 -0
- package/dist/resolve/__tests__/defaults.test.js.map +1 -0
- package/dist/resolve/__tests__/order.test.d.ts +2 -0
- package/dist/resolve/__tests__/order.test.d.ts.map +1 -0
- package/dist/resolve/__tests__/order.test.js +238 -0
- package/dist/resolve/__tests__/order.test.js.map +1 -0
- package/dist/resolve/__tests__/phase.test.d.ts +12 -0
- package/dist/resolve/__tests__/phase.test.d.ts.map +1 -0
- package/dist/resolve/__tests__/phase.test.js +81 -0
- package/dist/resolve/__tests__/phase.test.js.map +1 -0
- package/dist/resolve/defaults.d.ts +51 -0
- package/dist/resolve/defaults.d.ts.map +1 -0
- package/dist/resolve/defaults.js +109 -0
- package/dist/resolve/defaults.js.map +1 -0
- package/dist/resolve/order.d.ts +73 -0
- package/dist/resolve/order.d.ts.map +1 -0
- package/dist/resolve/order.js +208 -0
- package/dist/resolve/order.js.map +1 -0
- package/dist/resolve/phase.d.ts +57 -0
- package/dist/resolve/phase.d.ts.map +1 -0
- package/dist/resolve/phase.js +60 -0
- package/dist/resolve/phase.js.map +1 -0
- package/dist/scaffold/__tests__/scaffold.test.js +8 -2
- package/dist/scaffold/__tests__/scaffold.test.js.map +1 -1
- package/dist/scaffold/__tests__/settings-merge.test.js +95 -0
- package/dist/scaffold/__tests__/settings-merge.test.js.map +1 -1
- package/dist/scaffold/incremental.d.ts.map +1 -1
- package/dist/scaffold/incremental.js +13 -2
- package/dist/scaffold/incremental.js.map +1 -1
- package/dist/visualizer/__tests__/brain.test.d.ts +2 -0
- package/dist/visualizer/__tests__/brain.test.d.ts.map +1 -0
- package/dist/visualizer/__tests__/brain.test.js +138 -0
- package/dist/visualizer/__tests__/brain.test.js.map +1 -0
- package/dist/visualizer/__tests__/topology-brain-link.test.d.ts +2 -0
- package/dist/visualizer/__tests__/topology-brain-link.test.d.ts.map +1 -0
- package/dist/visualizer/__tests__/topology-brain-link.test.js +56 -0
- package/dist/visualizer/__tests__/topology-brain-link.test.js.map +1 -0
- package/dist/visualizer/brain.d.ts +97 -0
- package/dist/visualizer/brain.d.ts.map +1 -0
- package/dist/visualizer/brain.js +473 -0
- package/dist/visualizer/brain.js.map +1 -0
- package/dist/visualizer/index.d.ts +19 -1
- package/dist/visualizer/index.d.ts.map +1 -1
- package/dist/visualizer/index.js +26 -11
- package/dist/visualizer/index.js.map +1 -1
- package/examples/code-review.at +32 -4
- package/examples/company-brain-team.at +241 -0
- package/examples/company-brain.at +104 -0
- package/examples/data-processing.at +33 -4
- package/examples/logos/gmail.svg +1 -0
- package/examples/logos/slack.svg +1 -0
- package/package.json +1 -1
- package/spec/grammar.md +55 -3
- package/spec/validation.md +103 -35
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { resolve, dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { parse, parseSchemaType, parseSchemaFields, parseParams, parseInterfaceEndpoints, parseImports, parseIncludes } from "../index.js";
|
|
@@ -223,7 +223,14 @@ describe("Lexer", () => {
|
|
|
223
223
|
describe("Parser sections", () => {
|
|
224
224
|
describe("meta", () => {
|
|
225
225
|
it("parses version, description, and patterns from header", () => {
|
|
226
|
-
const src = `topology test : [pipeline] {\n meta {
|
|
226
|
+
const src = `topology test : [pipeline] {\n meta {
|
|
227
|
+
version: "2.0.0"\n
|
|
228
|
+
description: "A test topology"\n
|
|
229
|
+
domain: legal\n
|
|
230
|
+
}\n orchestrator {
|
|
231
|
+
model: opus\n
|
|
232
|
+
handles: [intake]\n
|
|
233
|
+
}\n action intake {\n kind: inline\n }\n flow {\n intake -> intake\n }\n}`;
|
|
227
234
|
const ast = parse(src);
|
|
228
235
|
expect(ast.topology.name).toBe("test");
|
|
229
236
|
expect(ast.topology.version).toBe("2.0.0");
|
|
@@ -232,7 +239,14 @@ describe("Parser sections", () => {
|
|
|
232
239
|
expect(ast.topology.domain).toBe("legal");
|
|
233
240
|
});
|
|
234
241
|
it("parses foundations and advanced lists", () => {
|
|
235
|
-
const src = `topology t : [pipeline] {\n meta {
|
|
242
|
+
const src = `topology t : [pipeline] {\n meta {
|
|
243
|
+
version: "1.0.0"\n
|
|
244
|
+
foundations: [pipeline, fan-out]\n
|
|
245
|
+
advanced: [consensus]\n
|
|
246
|
+
}\n orchestrator {
|
|
247
|
+
model: opus\n
|
|
248
|
+
handles: [a]\n
|
|
249
|
+
}\n action a { kind: inline }\n flow { a -> a }\n}`;
|
|
236
250
|
const ast = parse(src);
|
|
237
251
|
expect(ast.topology.foundations).toEqual(["pipeline", "fan-out"]);
|
|
238
252
|
expect(ast.topology.advanced).toEqual(["consensus"]);
|
|
@@ -252,7 +266,10 @@ describe("Parser sections", () => {
|
|
|
252
266
|
});
|
|
253
267
|
describe("roles", () => {
|
|
254
268
|
it("parses multiple role definitions", () => {
|
|
255
|
-
const src = `topology t : [pipeline] {\n roles {\n writer: "Writes drafts"\n reviewer: "Reviews content"\n }\n orchestrator {
|
|
269
|
+
const src = `topology t : [pipeline] {\n roles {\n writer: "Writes drafts"\n reviewer: "Reviews content"\n }\n orchestrator {
|
|
270
|
+
model: opus\n
|
|
271
|
+
handles: [a]
|
|
272
|
+
}\n action a { kind: inline }\n flow { a -> a }\n}`;
|
|
256
273
|
const ast = parse(src);
|
|
257
274
|
expect(ast.roles).toEqual({
|
|
258
275
|
writer: "Writes drafts",
|
|
@@ -262,7 +279,13 @@ describe("Parser sections", () => {
|
|
|
262
279
|
});
|
|
263
280
|
describe("action", () => {
|
|
264
281
|
it("parses inline action", () => {
|
|
265
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
282
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
283
|
+
model: opus\n
|
|
284
|
+
handles: [a]
|
|
285
|
+
}\n action a {
|
|
286
|
+
kind: inline\n
|
|
287
|
+
description: "Parse request"\n
|
|
288
|
+
}\n flow { a -> a }\n}`;
|
|
266
289
|
const ast = parse(src);
|
|
267
290
|
const action = ast.nodes.find((n) => n.id === "a");
|
|
268
291
|
expect(action).toBeDefined();
|
|
@@ -273,7 +296,13 @@ describe("Parser sections", () => {
|
|
|
273
296
|
}
|
|
274
297
|
});
|
|
275
298
|
it("parses external action with source", () => {
|
|
276
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
299
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
300
|
+
model: opus\n
|
|
301
|
+
handles: [fetch]
|
|
302
|
+
}\n action fetch {
|
|
303
|
+
kind: external\n
|
|
304
|
+
source: "github-pr"\n
|
|
305
|
+
}\n flow { fetch -> fetch }\n}`;
|
|
277
306
|
const ast = parse(src);
|
|
278
307
|
const action = ast.nodes.find((n) => n.id === "fetch");
|
|
279
308
|
if (action.type === "action") {
|
|
@@ -282,7 +311,13 @@ describe("Parser sections", () => {
|
|
|
282
311
|
}
|
|
283
312
|
});
|
|
284
313
|
it("parses action with commands list", () => {
|
|
285
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
314
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
315
|
+
model: opus\n
|
|
316
|
+
handles: [build]
|
|
317
|
+
}\n action build {
|
|
318
|
+
kind: inline\n
|
|
319
|
+
commands: [npm install, npm test]\n
|
|
320
|
+
}\n flow { build -> build }\n}`;
|
|
286
321
|
const ast = parse(src);
|
|
287
322
|
const action = ast.nodes.find((n) => n.id === "build");
|
|
288
323
|
if (action.type === "action") {
|
|
@@ -292,7 +327,25 @@ describe("Parser sections", () => {
|
|
|
292
327
|
});
|
|
293
328
|
describe("agent", () => {
|
|
294
329
|
it("parses all basic fields", () => {
|
|
295
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
330
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
331
|
+
model: opus\n
|
|
332
|
+
handles: [a]
|
|
333
|
+
}\n action a { kind: inline }\n agent my-agent {
|
|
334
|
+
model: sonnet\n
|
|
335
|
+
phase: 2\n
|
|
336
|
+
tools: [Read, Write]\n
|
|
337
|
+
reads: ["workspace/input"]\n
|
|
338
|
+
writes: ["workspace/output"]\n
|
|
339
|
+
permissions: auto\n
|
|
340
|
+
behavior: advisory\n
|
|
341
|
+
invocation: manual\n
|
|
342
|
+
isolation: worktree\n
|
|
343
|
+
background: true\n
|
|
344
|
+
retry: 3\n
|
|
345
|
+
skip: "depth < 2"\n
|
|
346
|
+
description: "Test agent"\n
|
|
347
|
+
max-turns: 10\n
|
|
348
|
+
}\n flow { a -> a }\n}`;
|
|
296
349
|
const ast = parse(src);
|
|
297
350
|
const agent = ast.nodes.find((n) => n.id === "my-agent");
|
|
298
351
|
expect(agent).toBeDefined();
|
|
@@ -312,7 +365,10 @@ describe("Parser sections", () => {
|
|
|
312
365
|
expect(agent.maxTurns).toBe(10);
|
|
313
366
|
});
|
|
314
367
|
it("parses inline prompt block", () => {
|
|
315
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
368
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
369
|
+
model: opus\n
|
|
370
|
+
handles: [a]
|
|
371
|
+
}\n action a { kind: inline }\n agent writer {\n model: opus\n prompt {\n You are a writer.\n Write good content.\n }\n }\n flow { a -> writer }\n}`;
|
|
316
372
|
const ast = parse(src);
|
|
317
373
|
const agent = ast.nodes.find((n) => n.id === "writer");
|
|
318
374
|
expect(agent.prompt).toBeDefined();
|
|
@@ -320,19 +376,31 @@ describe("Parser sections", () => {
|
|
|
320
376
|
expect(agent.prompt).toContain("Write good content.");
|
|
321
377
|
});
|
|
322
378
|
it("parses disallowed-tools", () => {
|
|
323
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
379
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
380
|
+
model: opus\n
|
|
381
|
+
handles: [a]
|
|
382
|
+
}\n action a { kind: inline }\n agent safe {
|
|
383
|
+
model: opus\n
|
|
384
|
+
disallowed-tools: [Bash, Write]\n
|
|
385
|
+
}\n flow { a -> safe }\n}`;
|
|
324
386
|
const ast = parse(src);
|
|
325
387
|
const agent = ast.nodes.find((n) => n.id === "safe");
|
|
326
388
|
expect(agent.disallowedTools).toEqual(["Bash", "Write"]);
|
|
327
389
|
});
|
|
328
390
|
it("parses outputs block", () => {
|
|
329
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
391
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
392
|
+
model: opus\n
|
|
393
|
+
handles: [a]
|
|
394
|
+
}\n action a { kind: inline }\n agent scorer {\n model: opus\n outputs {\n quality: high | medium | low\n }\n }\n flow { a -> scorer }\n}`;
|
|
330
395
|
const ast = parse(src);
|
|
331
396
|
const agent = ast.nodes.find((n) => n.id === "scorer");
|
|
332
397
|
expect(agent.outputs).toEqual({ quality: ["high", "medium", "low"] });
|
|
333
398
|
});
|
|
334
399
|
it("parses scale block", () => {
|
|
335
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
400
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
401
|
+
model: opus\n
|
|
402
|
+
handles: [a]
|
|
403
|
+
}\n action a { kind: inline }\n agent worker {\n model: opus\n scale {\n mode: auto\n by: doc-count\n min: 2\n max: 8\n batch-size: 50\n }\n }\n flow { a -> worker }\n}`;
|
|
336
404
|
const ast = parse(src);
|
|
337
405
|
const agent = ast.nodes.find((n) => n.id === "worker");
|
|
338
406
|
expect(agent.scale).toEqual({
|
|
@@ -344,7 +412,16 @@ describe("Parser sections", () => {
|
|
|
344
412
|
});
|
|
345
413
|
});
|
|
346
414
|
it("parses per-agent hooks block", () => {
|
|
347
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
415
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
416
|
+
model: opus\n
|
|
417
|
+
handles: [a]
|
|
418
|
+
}\n action a { kind: inline }\n agent hooked {\n model: opus\n hooks {\n hook pre-tool {
|
|
419
|
+
on: PreToolUse\n
|
|
420
|
+
matcher: "Bash"\n
|
|
421
|
+
run: "scripts/check.sh"\n
|
|
422
|
+
type: command\n
|
|
423
|
+
timeout: 5000\n
|
|
424
|
+
}\n }\n }\n flow { a -> hooked }\n}`;
|
|
348
425
|
const ast = parse(src);
|
|
349
426
|
const agent = ast.nodes.find((n) => n.id === "hooked");
|
|
350
427
|
expect(agent.hooks).toBeDefined();
|
|
@@ -357,7 +434,10 @@ describe("Parser sections", () => {
|
|
|
357
434
|
expect(agent.hooks[0].timeout).toBe(5000);
|
|
358
435
|
});
|
|
359
436
|
it("parses extensions block", () => {
|
|
360
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
437
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
438
|
+
model: opus\n
|
|
439
|
+
handles: [a]
|
|
440
|
+
}\n action a { kind: inline }\n agent extended {\n model: opus\n extensions {\n openclaw {\n calendar-integration: true\n port: 8080\n }\n }\n }\n flow { a -> extended }\n}`;
|
|
361
441
|
const ast = parse(src);
|
|
362
442
|
const agent = ast.nodes.find((n) => n.id === "extended");
|
|
363
443
|
expect(agent.extensions).toBeDefined();
|
|
@@ -367,19 +447,34 @@ describe("Parser sections", () => {
|
|
|
367
447
|
});
|
|
368
448
|
});
|
|
369
449
|
it("parses mcp-servers list", () => {
|
|
370
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
450
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
451
|
+
model: opus\n
|
|
452
|
+
handles: [a]
|
|
453
|
+
}\n action a { kind: inline }\n agent mcp-user {
|
|
454
|
+
model: opus\n
|
|
455
|
+
mcp-servers: [storage, monitoring]\n
|
|
456
|
+
}\n flow { a -> mcp-user }\n}`;
|
|
371
457
|
const ast = parse(src);
|
|
372
458
|
const agent = ast.nodes.find((n) => n.id === "mcp-user");
|
|
373
459
|
expect(agent.mcpServers).toEqual(["storage", "monitoring"]);
|
|
374
460
|
});
|
|
375
461
|
it("parses skills list", () => {
|
|
376
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
462
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
463
|
+
model: opus\n
|
|
464
|
+
handles: [a]
|
|
465
|
+
}\n action a { kind: inline }\n agent skilled {
|
|
466
|
+
model: opus\n
|
|
467
|
+
skills: [extraction, classification]\n
|
|
468
|
+
}\n flow { a -> skilled }\n}`;
|
|
377
469
|
const ast = parse(src);
|
|
378
470
|
const agent = ast.nodes.find((n) => n.id === "skilled");
|
|
379
471
|
expect(agent.skills).toEqual(["extraction", "classification"]);
|
|
380
472
|
});
|
|
381
473
|
it("attaches role from roles block", () => {
|
|
382
|
-
const src = `topology t : [pipeline] {\n roles {\n writer: "Writes things"\n }\n orchestrator {
|
|
474
|
+
const src = `topology t : [pipeline] {\n roles {\n writer: "Writes things"\n }\n orchestrator {
|
|
475
|
+
model: opus\n
|
|
476
|
+
handles: [a]
|
|
477
|
+
}\n action a { kind: inline }\n agent writer {\n model: opus\n }\n flow { a -> writer }\n}`;
|
|
383
478
|
const ast = parse(src);
|
|
384
479
|
const agent = ast.nodes.find((n) => n.id === "writer");
|
|
385
480
|
expect(agent.role).toBe("Writes things");
|
|
@@ -387,14 +482,20 @@ describe("Parser sections", () => {
|
|
|
387
482
|
});
|
|
388
483
|
describe("flow", () => {
|
|
389
484
|
it("parses simple chain", () => {
|
|
390
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
485
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
486
|
+
model: opus\n
|
|
487
|
+
handles: [a, c]
|
|
488
|
+
}\n action a { kind: inline }\n agent b { model: opus }\n action c { kind: report }\n flow {\n a -> b -> c\n }\n}`;
|
|
391
489
|
const ast = parse(src);
|
|
392
490
|
expect(ast.edges).toHaveLength(2);
|
|
393
491
|
expect(ast.edges[0]).toEqual({ from: "a", to: "b", condition: null, maxIterations: null, per: null });
|
|
394
492
|
expect(ast.edges[1]).toEqual({ from: "b", to: "c", condition: null, maxIterations: null, per: null });
|
|
395
493
|
});
|
|
396
494
|
it("parses fan-out [a, b]", () => {
|
|
397
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
495
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
496
|
+
model: opus\n
|
|
497
|
+
handles: [start]
|
|
498
|
+
}\n action start { kind: inline }\n agent x { model: opus }\n agent y { model: opus }\n flow {\n start -> [x, y]\n }\n}`;
|
|
398
499
|
const ast = parse(src);
|
|
399
500
|
expect(ast.edges).toHaveLength(2);
|
|
400
501
|
expect(ast.edges[0].from).toBe("start");
|
|
@@ -403,7 +504,10 @@ describe("Parser sections", () => {
|
|
|
403
504
|
expect(ast.edges[1].to).toBe("y");
|
|
404
505
|
});
|
|
405
506
|
it("parses conditions [when x.y == z]", () => {
|
|
406
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
507
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
508
|
+
model: opus\n
|
|
509
|
+
handles: [a]
|
|
510
|
+
}\n action a { kind: inline }\n agent b { model: opus\n outputs { verdict: yes | no } }\n agent c { model: opus }\n flow {\n b -> c [when b.verdict == yes]\n }\n}`;
|
|
407
511
|
const ast = parse(src);
|
|
408
512
|
const edge = ast.edges.find((e) => e.from === "b" && e.to === "c");
|
|
409
513
|
expect(edge).toBeDefined();
|
|
@@ -411,7 +515,10 @@ describe("Parser sections", () => {
|
|
|
411
515
|
expect(edge.maxIterations).toBeNull();
|
|
412
516
|
});
|
|
413
517
|
it("parses bounded loops [max N]", () => {
|
|
414
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
518
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
519
|
+
model: opus\n
|
|
520
|
+
handles: [a]
|
|
521
|
+
}\n action a { kind: inline }\n agent b { model: opus }\n agent c { model: opus }\n flow {\n c -> b [max 3]\n }\n}`;
|
|
415
522
|
const ast = parse(src);
|
|
416
523
|
const edge = ast.edges.find((e) => e.from === "c" && e.to === "b");
|
|
417
524
|
expect(edge).toBeDefined();
|
|
@@ -419,7 +526,10 @@ describe("Parser sections", () => {
|
|
|
419
526
|
expect(edge.condition).toBeNull();
|
|
420
527
|
});
|
|
421
528
|
it("parses combined [when x.y == z, max 3]", () => {
|
|
422
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
529
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
530
|
+
model: opus\n
|
|
531
|
+
handles: [a]
|
|
532
|
+
}\n action a { kind: inline }\n agent b { model: opus\n outputs { verdict: yes | no } }\n agent c { model: opus }\n flow {\n c -> b [when c.verdict == no, max 3]\n }\n}`;
|
|
423
533
|
const ast = parse(src);
|
|
424
534
|
const edge = ast.edges.find((e) => e.from === "c" && e.to === "b");
|
|
425
535
|
expect(edge).toBeDefined();
|
|
@@ -429,7 +539,18 @@ describe("Parser sections", () => {
|
|
|
429
539
|
});
|
|
430
540
|
describe("gates", () => {
|
|
431
541
|
it("parses gate with all fields", () => {
|
|
432
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
542
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
543
|
+
model: opus\n
|
|
544
|
+
handles: [a]
|
|
545
|
+
}\n action a { kind: inline }\n agent b { model: opus }\n agent c { model: opus }\n gates {\n gate quality {
|
|
546
|
+
after: b\n
|
|
547
|
+
before: c\n
|
|
548
|
+
run: "scripts/check.sh"\n
|
|
549
|
+
checks: [grammar, formatting]\n
|
|
550
|
+
on-fail: bounce-back\n
|
|
551
|
+
retry: 2\n
|
|
552
|
+
behavior: blocking\n
|
|
553
|
+
}\n }\n flow { a -> b -> c }\n}`;
|
|
433
554
|
const ast = parse(src);
|
|
434
555
|
const gate = ast.nodes.find((n) => n.id === "quality");
|
|
435
556
|
expect(gate).toBeDefined();
|
|
@@ -443,7 +564,10 @@ describe("Parser sections", () => {
|
|
|
443
564
|
expect(gate.behavior).toBe("blocking");
|
|
444
565
|
});
|
|
445
566
|
it("parses gate with extensions", () => {
|
|
446
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
567
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
568
|
+
model: opus\n
|
|
569
|
+
handles: [a]
|
|
570
|
+
}\n action a { kind: inline }\n agent b { model: opus }\n gates {\n gate ext-gate {\n after: b\n run: "check.sh"\n extensions {\n custom {\n enabled: true\n }\n }\n }\n }\n flow { a -> b }\n}`;
|
|
447
571
|
const ast = parse(src);
|
|
448
572
|
const gate = ast.nodes.find((n) => n.id === "ext-gate");
|
|
449
573
|
expect(gate.extensions).toBeDefined();
|
|
@@ -452,7 +576,10 @@ describe("Parser sections", () => {
|
|
|
452
576
|
});
|
|
453
577
|
describe("depth", () => {
|
|
454
578
|
it("parses levels and factors", () => {
|
|
455
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
579
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
580
|
+
model: opus\n
|
|
581
|
+
handles: [a]
|
|
582
|
+
}\n action a { kind: inline }\n flow { a -> a }\n depth {\n factors: [batch-count, token-volume]\n level 1 "Quick" {\n omit: [reviewer]\n }\n level 2 "Full" {\n omit: []\n }\n }\n}`;
|
|
456
583
|
const ast = parse(src);
|
|
457
584
|
expect(ast.depth.factors).toEqual(["batch-count", "token-volume"]);
|
|
458
585
|
expect(ast.depth.levels).toHaveLength(2);
|
|
@@ -462,7 +589,10 @@ describe("Parser sections", () => {
|
|
|
462
589
|
});
|
|
463
590
|
describe("memory", () => {
|
|
464
591
|
it("parses workspace, domains, and metrics", () => {
|
|
465
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
592
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
593
|
+
model: opus\n
|
|
594
|
+
handles: [a]
|
|
595
|
+
}\n action a { kind: inline }\n flow { a -> a }\n memory {\n workspace {\n path: "workspace/"\n protocol: "proto.md"\n structure: [raw, processed]\n }\n domains {\n path: "domains/"\n routing: "FILE_MAP.md"\n }\n metrics {\n path: "metrics.jsonl"\n mode: append-only\n }\n }\n}`;
|
|
466
596
|
const ast = parse(src);
|
|
467
597
|
const ws = ast.memory["workspace"];
|
|
468
598
|
expect(ws["path"]).toBe("workspace/");
|
|
@@ -478,7 +608,10 @@ describe("Parser sections", () => {
|
|
|
478
608
|
});
|
|
479
609
|
describe("batch", () => {
|
|
480
610
|
it("parses parallel, per, workspace, and conflicts", () => {
|
|
481
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
611
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
612
|
+
model: opus\n
|
|
613
|
+
handles: [a]
|
|
614
|
+
}\n action a { kind: inline }\n flow { a -> a }\n batch {\n parallel: true\n per: ticket\n workspace: "runs/{BATCH_ID}/"\n conflicts {\n detect: ["workspace/out.json"]\n resolve: sequential-rebase\n }\n }\n}`;
|
|
482
615
|
const ast = parse(src);
|
|
483
616
|
expect(ast.batch["parallel"]).toBe(true);
|
|
484
617
|
expect(ast.batch["per"]).toBe("ticket");
|
|
@@ -490,7 +623,10 @@ describe("Parser sections", () => {
|
|
|
490
623
|
});
|
|
491
624
|
describe("environments", () => {
|
|
492
625
|
it("parses multiple named environment blocks", () => {
|
|
493
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
626
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
627
|
+
model: opus\n
|
|
628
|
+
handles: [a]
|
|
629
|
+
}\n action a { kind: inline }\n flow { a -> a }\n environments {\n staging {\n bucket: "stage-bucket"\n }\n production {\n bucket: "prod-bucket"\n }\n }\n}`;
|
|
494
630
|
const ast = parse(src);
|
|
495
631
|
expect(ast.environments["staging"]).toEqual({ bucket: "stage-bucket" });
|
|
496
632
|
expect(ast.environments["production"]).toEqual({ bucket: "prod-bucket" });
|
|
@@ -498,7 +634,10 @@ describe("Parser sections", () => {
|
|
|
498
634
|
});
|
|
499
635
|
describe("triggers", () => {
|
|
500
636
|
it("parses command with pattern", () => {
|
|
501
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
637
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
638
|
+
model: opus\n
|
|
639
|
+
handles: [a]
|
|
640
|
+
}\n action a { kind: inline }\n flow { a -> a }\n triggers {\n command audit {\n pattern: "/audit"\n }\n }\n}`;
|
|
502
641
|
const ast = parse(src);
|
|
503
642
|
expect(ast.triggers).toHaveLength(1);
|
|
504
643
|
expect(ast.triggers[0].name).toBe("audit");
|
|
@@ -506,7 +645,10 @@ describe("Parser sections", () => {
|
|
|
506
645
|
expect(ast.triggers[0].argument).toBeUndefined();
|
|
507
646
|
});
|
|
508
647
|
it("parses command with pattern and argument", () => {
|
|
509
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
648
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
649
|
+
model: opus\n
|
|
650
|
+
handles: [a]
|
|
651
|
+
}\n action a { kind: inline }\n flow { a -> a }\n triggers {\n command create {\n pattern: "/create <TOPIC>"\n argument: TOPIC\n }\n }\n}`;
|
|
510
652
|
const ast = parse(src);
|
|
511
653
|
expect(ast.triggers[0].name).toBe("create");
|
|
512
654
|
expect(ast.triggers[0].pattern).toBe("/create <TOPIC>");
|
|
@@ -515,7 +657,16 @@ describe("Parser sections", () => {
|
|
|
515
657
|
});
|
|
516
658
|
describe("hooks", () => {
|
|
517
659
|
it("parses hooks with on, run, type, matcher, timeout", () => {
|
|
518
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
660
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
661
|
+
model: opus\n
|
|
662
|
+
handles: [a]
|
|
663
|
+
}\n action a { kind: inline }\n flow { a -> a }\n hooks {\n hook my-hook {
|
|
664
|
+
on: PreToolUse\n
|
|
665
|
+
matcher: "Bash"\n
|
|
666
|
+
run: "scripts/guard.sh"\n
|
|
667
|
+
type: command\n
|
|
668
|
+
timeout: 3000\n
|
|
669
|
+
}\n }\n}`;
|
|
519
670
|
const ast = parse(src);
|
|
520
671
|
expect(ast.hooks).toHaveLength(1);
|
|
521
672
|
expect(ast.hooks[0].name).toBe("my-hook");
|
|
@@ -528,7 +679,10 @@ describe("Parser sections", () => {
|
|
|
528
679
|
});
|
|
529
680
|
describe("settings", () => {
|
|
530
681
|
it("parses allow, deny, ask lists", () => {
|
|
531
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
682
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
683
|
+
model: opus\n
|
|
684
|
+
handles: [a]
|
|
685
|
+
}\n action a { kind: inline }\n flow { a -> a }\n settings {\n allow: [Read, Write]\n deny: [Bash]\n ask: [Edit]\n }\n}`;
|
|
532
686
|
const ast = parse(src);
|
|
533
687
|
expect(ast.settings["allow"]).toEqual(["Read", "Write"]);
|
|
534
688
|
expect(ast.settings["deny"]).toEqual(["Bash"]);
|
|
@@ -537,7 +691,10 @@ describe("Parser sections", () => {
|
|
|
537
691
|
});
|
|
538
692
|
describe("mcp-servers", () => {
|
|
539
693
|
it("parses stdio and http server types", () => {
|
|
540
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
694
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
695
|
+
model: opus\n
|
|
696
|
+
handles: [a]
|
|
697
|
+
}\n action a { kind: inline }\n flow { a -> a }\n mcp-servers {\n storage {\n type: stdio\n command: "npx"\n args: ["-y", "storage-server"]\n }\n monitor {\n type: http\n url: "https://mcp.example.com"\n }\n }\n}`;
|
|
541
698
|
const ast = parse(src);
|
|
542
699
|
expect(ast.mcpServers["storage"]).toBeDefined();
|
|
543
700
|
expect(ast.mcpServers["storage"]["type"]).toBe("stdio");
|
|
@@ -549,7 +706,10 @@ describe("Parser sections", () => {
|
|
|
549
706
|
});
|
|
550
707
|
describe("metering", () => {
|
|
551
708
|
it("parses track, per, output, format, pricing", () => {
|
|
552
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
709
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
710
|
+
model: opus\n
|
|
711
|
+
handles: [a]
|
|
712
|
+
}\n action a { kind: inline }\n flow { a -> a }\n metering {\n track: [tokens-in, tokens-out, cost]\n per: [agent, run]\n output: "metrics/"\n format: jsonl\n pricing: anthropic-current\n }\n}`;
|
|
553
713
|
const ast = parse(src);
|
|
554
714
|
expect(ast.metering).not.toBeNull();
|
|
555
715
|
expect(ast.metering.track).toEqual(["tokens-in", "tokens-out", "cost"]);
|
|
@@ -561,7 +721,10 @@ describe("Parser sections", () => {
|
|
|
561
721
|
});
|
|
562
722
|
describe("tools", () => {
|
|
563
723
|
it("parses tool with script, args, lang, description", () => {
|
|
564
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
724
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
725
|
+
model: opus\n
|
|
726
|
+
handles: [a]
|
|
727
|
+
}\n action a { kind: inline }\n flow { a -> a }\n tools {\n tool extract-pdf {\n script: "scripts/extract.py"\n args: [input, output]\n lang: python\n description: "Extract text from PDF"\n }\n }\n}`;
|
|
565
728
|
const ast = parse(src);
|
|
566
729
|
expect(ast.toolDefs).toHaveLength(1);
|
|
567
730
|
expect(ast.toolDefs[0].id).toBe("extract-pdf");
|
|
@@ -573,7 +736,10 @@ describe("Parser sections", () => {
|
|
|
573
736
|
});
|
|
574
737
|
describe("skill", () => {
|
|
575
738
|
it("parses all skill fields", () => {
|
|
576
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
739
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
740
|
+
model: opus\n
|
|
741
|
+
handles: [a]
|
|
742
|
+
}\n action a { kind: inline }\n flow { a -> a }\n skill my-skill {\n description: "A test skill"\n scripts: [script-a, script-b]\n domains: ["domains/rubric.md"]\n references: ["ref/guide.md"]\n prompt: "prompts/skill.md"\n disable-model-invocation: true\n user-invocable: false\n context: fork\n agent: task-agent\n allowed-tools: [Read, Write]\n extensions {\n custom {\n key: value\n }\n }\n }\n}`;
|
|
577
743
|
const ast = parse(src);
|
|
578
744
|
expect(ast.skills).toHaveLength(1);
|
|
579
745
|
const skill = ast.skills[0];
|
|
@@ -594,7 +760,10 @@ describe("Parser sections", () => {
|
|
|
594
760
|
});
|
|
595
761
|
describe("context", () => {
|
|
596
762
|
it("parses file and includes", () => {
|
|
597
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
763
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
764
|
+
model: opus\n
|
|
765
|
+
handles: [a]
|
|
766
|
+
}\n action a { kind: inline }\n flow { a -> a }\n context {\n file: "CONTEXT.md"\n includes: [README.md, CONTRIBUTING.md]\n }\n}`;
|
|
598
767
|
const ast = parse(src);
|
|
599
768
|
expect(ast.context.file).toBe("CONTEXT.md");
|
|
600
769
|
expect(ast.context.includes).toEqual(["README.md", "CONTRIBUTING.md"]);
|
|
@@ -602,7 +771,10 @@ describe("Parser sections", () => {
|
|
|
602
771
|
});
|
|
603
772
|
describe("env", () => {
|
|
604
773
|
it("parses key-value pairs", () => {
|
|
605
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
774
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
775
|
+
model: opus\n
|
|
776
|
+
handles: [a]
|
|
777
|
+
}\n action a { kind: inline }\n flow { a -> a }\n env {\n API_KEY: "sk-123"\n REGION: "us-east-1"\n }\n}`;
|
|
606
778
|
const ast = parse(src);
|
|
607
779
|
expect(ast.env["API_KEY"]).toBe("sk-123");
|
|
608
780
|
expect(ast.env["REGION"]).toBe("us-east-1");
|
|
@@ -610,7 +782,10 @@ describe("Parser sections", () => {
|
|
|
610
782
|
});
|
|
611
783
|
describe("providers", () => {
|
|
612
784
|
it("parses provider with all fields", () => {
|
|
613
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
785
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
786
|
+
model: opus\n
|
|
787
|
+
handles: [a]
|
|
788
|
+
}\n action a { kind: inline }\n flow { a -> a }\n providers {\n anthropic {\n api-key: "\${ANTHROPIC_API_KEY}"\n base-url: "https://api.anthropic.com"\n models: [opus, sonnet, haiku]\n default: true\n }\n openai {\n api-key: "\${OPENAI_API_KEY}"\n models: [gpt-4o]\n }\n }\n}`;
|
|
614
789
|
const ast = parse(src);
|
|
615
790
|
expect(ast.providers).toHaveLength(2);
|
|
616
791
|
expect(ast.providers[0].name).toBe("anthropic");
|
|
@@ -625,7 +800,13 @@ describe("Parser sections", () => {
|
|
|
625
800
|
});
|
|
626
801
|
describe("schedule", () => {
|
|
627
802
|
it("parses job with cron", () => {
|
|
628
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
803
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
804
|
+
model: opus\n
|
|
805
|
+
handles: [a]
|
|
806
|
+
}\n action a { kind: inline }\n agent worker { model: opus }\n flow { a -> worker }\n schedule {\n job nightly {
|
|
807
|
+
cron: "0 0 * * *"\n
|
|
808
|
+
agent: worker\n
|
|
809
|
+
}\n }\n}`;
|
|
629
810
|
const ast = parse(src);
|
|
630
811
|
expect(ast.schedules).toHaveLength(1);
|
|
631
812
|
expect(ast.schedules[0].id).toBe("nightly");
|
|
@@ -634,7 +815,14 @@ describe("Parser sections", () => {
|
|
|
634
815
|
expect(ast.schedules[0].enabled).toBe(true);
|
|
635
816
|
});
|
|
636
817
|
it("parses job with every", () => {
|
|
637
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
818
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
819
|
+
model: opus\n
|
|
820
|
+
handles: [a]
|
|
821
|
+
}\n action a { kind: inline }\n agent worker { model: opus }\n flow { a -> worker }\n schedule {\n job weekly {
|
|
822
|
+
every: "monday 9:00"\n
|
|
823
|
+
agent: worker\n
|
|
824
|
+
enabled: true\n
|
|
825
|
+
}\n }\n}`;
|
|
638
826
|
const ast = parse(src);
|
|
639
827
|
expect(ast.schedules).toHaveLength(1);
|
|
640
828
|
expect(ast.schedules[0].id).toBe("weekly");
|
|
@@ -643,14 +831,24 @@ describe("Parser sections", () => {
|
|
|
643
831
|
expect(ast.schedules[0].enabled).toBe(true);
|
|
644
832
|
});
|
|
645
833
|
it("parses job with enabled: false", () => {
|
|
646
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
834
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
835
|
+
model: opus\n
|
|
836
|
+
handles: [a]
|
|
837
|
+
}\n action a { kind: inline }\n agent worker { model: opus }\n flow { a -> worker }\n schedule {\n job disabled-job {
|
|
838
|
+
cron: "0 0 * * *"\n
|
|
839
|
+
agent: worker\n
|
|
840
|
+
enabled: false\n
|
|
841
|
+
}\n }\n}`;
|
|
647
842
|
const ast = parse(src);
|
|
648
843
|
expect(ast.schedules[0].enabled).toBe(false);
|
|
649
844
|
});
|
|
650
845
|
});
|
|
651
846
|
describe("interfaces", () => {
|
|
652
847
|
it("parses multiple interfaces with type and config", () => {
|
|
653
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
848
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
849
|
+
model: opus\n
|
|
850
|
+
handles: [a]
|
|
851
|
+
}\n action a { kind: inline }\n flow { a -> a }\n interfaces {\n slack {\n type: webhook\n webhook: "\${SLACK_URL}"\n channel: "alerts"\n }\n dashboard {\n type: http\n port: 8080\n }\n }\n}`;
|
|
654
852
|
const ast = parse(src);
|
|
655
853
|
expect(ast.interfaces).toHaveLength(2);
|
|
656
854
|
const slack = ast.interfaces.find((i) => i.id === "slack");
|
|
@@ -666,32 +864,56 @@ describe("Parser sections", () => {
|
|
|
666
864
|
});
|
|
667
865
|
describe("sandbox", () => {
|
|
668
866
|
it("parses sandbox field on agent (string value)", () => {
|
|
669
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
867
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
868
|
+
model: opus\n
|
|
869
|
+
handles: [a]
|
|
870
|
+
}\n action a { kind: inline }\n agent sandboxed {
|
|
871
|
+
model: opus\n
|
|
872
|
+
sandbox: docker
|
|
873
|
+
}\n flow { a -> sandboxed }\n}`;
|
|
670
874
|
const ast = parse(src);
|
|
671
875
|
const agent = ast.nodes.find((n) => n.id === "sandboxed");
|
|
672
876
|
expect(agent.sandbox).toBe("docker");
|
|
673
877
|
});
|
|
674
878
|
it("parses sandbox: true on agent", () => {
|
|
675
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
879
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
880
|
+
model: opus\n
|
|
881
|
+
handles: [a]
|
|
882
|
+
}\n action a { kind: inline }\n agent sandboxed {
|
|
883
|
+
model: opus\n
|
|
884
|
+
sandbox: true
|
|
885
|
+
}\n flow { a -> sandboxed }\n}`;
|
|
676
886
|
const ast = parse(src);
|
|
677
887
|
const agent = ast.nodes.find((n) => n.id === "sandboxed");
|
|
678
888
|
expect(agent.sandbox).toBe(true);
|
|
679
889
|
});
|
|
680
890
|
it("parses sandbox in settings", () => {
|
|
681
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
891
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
892
|
+
model: opus\n
|
|
893
|
+
handles: [a]
|
|
894
|
+
}\n action a { kind: inline }\n flow { a -> a }\n settings {\n sandbox: network-only\n }\n}`;
|
|
682
895
|
const ast = parse(src);
|
|
683
896
|
expect(ast.settings["sandbox"]).toBe("network-only");
|
|
684
897
|
});
|
|
685
898
|
});
|
|
686
899
|
describe("fallback-chain", () => {
|
|
687
900
|
it("parses fallback-chain on agent", () => {
|
|
688
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
901
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
902
|
+
model: opus\n
|
|
903
|
+
handles: [a]
|
|
904
|
+
}\n action a { kind: inline }\n agent resilient {
|
|
905
|
+
model: opus\n
|
|
906
|
+
fallback-chain: [sonnet, haiku]
|
|
907
|
+
}\n flow { a -> resilient }\n}`;
|
|
689
908
|
const ast = parse(src);
|
|
690
909
|
const agent = ast.nodes.find((n) => n.id === "resilient");
|
|
691
910
|
expect(agent.fallbackChain).toEqual(["sonnet", "haiku"]);
|
|
692
911
|
});
|
|
693
912
|
it("parses fallback-chain in settings", () => {
|
|
694
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
913
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
914
|
+
model: opus\n
|
|
915
|
+
handles: [a]
|
|
916
|
+
}\n action a { kind: inline }\n flow { a -> a }\n settings {\n fallback-chain: [opus, sonnet, haiku]\n }\n}`;
|
|
695
917
|
const ast = parse(src);
|
|
696
918
|
expect(ast.settings["fallbackChain"]).toEqual(["opus", "sonnet", "haiku"]);
|
|
697
919
|
});
|
|
@@ -1366,33 +1588,60 @@ describe("Integration: example files", () => {
|
|
|
1366
1588
|
// =========================================================================
|
|
1367
1589
|
describe("Edge cases", () => {
|
|
1368
1590
|
it("inline comments are stripped from agent model", () => {
|
|
1369
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1591
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1592
|
+
model: opus\n
|
|
1593
|
+
handles: [a]
|
|
1594
|
+
}\n action a { kind: inline }\n agent worker {
|
|
1595
|
+
model: opus # main model\n
|
|
1596
|
+
tools: [Read, Write] # core tools\n
|
|
1597
|
+
}\n flow { a -> worker }\n}`;
|
|
1370
1598
|
const ast = parse(src);
|
|
1371
1599
|
const agent = ast.nodes.find((n) => n.id === "worker");
|
|
1372
1600
|
expect(agent.model).toBe("opus");
|
|
1373
1601
|
expect(agent.tools).toEqual(["Read", "Write"]);
|
|
1374
1602
|
});
|
|
1375
1603
|
it("prompt blocks with # characters inside are preserved", () => {
|
|
1376
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1604
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1605
|
+
model: opus\n
|
|
1606
|
+
handles: [a]
|
|
1607
|
+
}\n action a { kind: inline }\n agent writer {\n model: opus\n prompt {\n # Step 1: Research\n Do research.\n # Step 2: Write\n Write content.\n }\n }\n flow { a -> writer }\n}`;
|
|
1377
1608
|
const ast = parse(src);
|
|
1378
1609
|
const agent = ast.nodes.find((n) => n.id === "writer");
|
|
1379
1610
|
expect(agent.prompt).toContain("# Step 1: Research");
|
|
1380
1611
|
expect(agent.prompt).toContain("# Step 2: Write");
|
|
1381
1612
|
});
|
|
1382
1613
|
it("nested braces in prompt blocks", () => {
|
|
1383
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1614
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1615
|
+
model: opus\n
|
|
1616
|
+
handles: [a]
|
|
1617
|
+
}\n action a { kind: inline }\n agent coder {\n model: opus\n prompt {\n Output JSON like:\n { "key": { "nested": true } }\n }\n }\n flow { a -> coder }\n}`;
|
|
1384
1618
|
const ast = parse(src);
|
|
1385
1619
|
const agent = ast.nodes.find((n) => n.id === "coder");
|
|
1386
1620
|
expect(agent.prompt).toContain('"key"');
|
|
1387
1621
|
});
|
|
1388
1622
|
it("agent with decimal phase numbers", () => {
|
|
1389
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1623
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1624
|
+
model: opus\n
|
|
1625
|
+
handles: [a]
|
|
1626
|
+
}\n action a { kind: inline }\n agent precise {
|
|
1627
|
+
model: opus\n
|
|
1628
|
+
phase: 4.5\n
|
|
1629
|
+
}\n flow { a -> precise }\n}`;
|
|
1390
1630
|
const ast = parse(src);
|
|
1391
1631
|
const agent = ast.nodes.find((n) => n.id === "precise");
|
|
1392
1632
|
expect(agent.phase).toBe(4.5);
|
|
1393
1633
|
});
|
|
1394
1634
|
it("multiple agents with same phase number", () => {
|
|
1395
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1635
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1636
|
+
model: opus\n
|
|
1637
|
+
handles: [a]
|
|
1638
|
+
}\n action a { kind: inline }\n agent p1 {
|
|
1639
|
+
model: opus\n
|
|
1640
|
+
phase: 1\n
|
|
1641
|
+
}\n agent p2 {
|
|
1642
|
+
model: opus\n
|
|
1643
|
+
phase: 1\n
|
|
1644
|
+
}\n flow { a -> [p1, p2] }\n}`;
|
|
1396
1645
|
const ast = parse(src);
|
|
1397
1646
|
const p1 = ast.nodes.find((n) => n.id === "p1");
|
|
1398
1647
|
const p2 = ast.nodes.find((n) => n.id === "p2");
|
|
@@ -1400,14 +1649,26 @@ describe("Edge cases", () => {
|
|
|
1400
1649
|
expect(p2.phase).toBe(1);
|
|
1401
1650
|
});
|
|
1402
1651
|
it("gate with only after (no before)", () => {
|
|
1403
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1652
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1653
|
+
model: opus\n
|
|
1654
|
+
handles: [a]
|
|
1655
|
+
}\n action a { kind: inline }\n agent b { model: opus }\n gates {\n gate after-only {
|
|
1656
|
+
after: b\n
|
|
1657
|
+
run: "check.sh"\n
|
|
1658
|
+
}\n }\n flow { a -> b }\n}`;
|
|
1404
1659
|
const ast = parse(src);
|
|
1405
1660
|
const gate = ast.nodes.find((n) => n.id === "after-only");
|
|
1406
1661
|
expect(gate.after).toBe("b");
|
|
1407
1662
|
expect(gate.before).toBeUndefined();
|
|
1408
1663
|
});
|
|
1409
1664
|
it("hook with only on and run (minimal)", () => {
|
|
1410
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1665
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1666
|
+
model: opus\n
|
|
1667
|
+
handles: [a]
|
|
1668
|
+
}\n action a { kind: inline }\n flow { a -> a }\n hooks {\n hook minimal {
|
|
1669
|
+
on: Stop\n
|
|
1670
|
+
run: "cleanup.sh"\n
|
|
1671
|
+
}\n }\n}`;
|
|
1411
1672
|
const ast = parse(src);
|
|
1412
1673
|
expect(ast.hooks).toHaveLength(1);
|
|
1413
1674
|
expect(ast.hooks[0].name).toBe("minimal");
|
|
@@ -1417,21 +1678,30 @@ describe("Edge cases", () => {
|
|
|
1417
1678
|
expect(ast.hooks[0].timeout).toBeUndefined();
|
|
1418
1679
|
});
|
|
1419
1680
|
it("empty sections produce reasonable defaults", () => {
|
|
1420
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1681
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1682
|
+
model: opus\n
|
|
1683
|
+
handles: [a]
|
|
1684
|
+
}\n action a { kind: inline }\n flow { a -> a }\n memory {\n }\n settings {\n }\n}`;
|
|
1421
1685
|
const ast = parse(src);
|
|
1422
1686
|
expect(ast.memory).toEqual({});
|
|
1423
1687
|
expect(ast.settings).toEqual({ allow: [], deny: [], ask: [] });
|
|
1424
1688
|
});
|
|
1425
1689
|
it("extremely long prompt block", () => {
|
|
1426
1690
|
const longContent = "This is a very long line. ".repeat(500);
|
|
1427
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1691
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1692
|
+
model: opus\n
|
|
1693
|
+
handles: [a]
|
|
1694
|
+
}\n action a { kind: inline }\n agent verbose {\n model: opus\n prompt {\n ${longContent}\n }\n }\n flow { a -> verbose }\n}`;
|
|
1428
1695
|
const ast = parse(src);
|
|
1429
1696
|
const agent = ast.nodes.find((n) => n.id === "verbose");
|
|
1430
1697
|
expect(agent.prompt).toBeDefined();
|
|
1431
1698
|
expect(agent.prompt.length).toBeGreaterThan(1000);
|
|
1432
1699
|
});
|
|
1433
1700
|
it("flow with combined [when x.y == z, max 3]", () => {
|
|
1434
|
-
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1701
|
+
const src = `topology t : [pipeline] {\n orchestrator {
|
|
1702
|
+
model: opus\n
|
|
1703
|
+
handles: [a]
|
|
1704
|
+
}\n action a { kind: inline }\n agent b { model: opus\n outputs { status: pass | fail } }\n agent c { model: opus }\n flow {\n c -> b [when b.status == fail, max 3]\n }\n}`;
|
|
1435
1705
|
const ast = parse(src);
|
|
1436
1706
|
const edge = ast.edges.find((e) => e.from === "c" && e.to === "b");
|
|
1437
1707
|
expect(edge).toBeDefined();
|
|
@@ -1465,7 +1735,10 @@ describe("Parse errors (should throw)", () => {
|
|
|
1465
1735
|
describe("Validation errors", () => {
|
|
1466
1736
|
it("V1 UniqueNames: two agents with the same id", () => {
|
|
1467
1737
|
const src = `topology t : [pipeline] {
|
|
1468
|
-
orchestrator {
|
|
1738
|
+
orchestrator {
|
|
1739
|
+
model: opus
|
|
1740
|
+
handles: [a]
|
|
1741
|
+
}
|
|
1469
1742
|
action a { kind: inline }
|
|
1470
1743
|
agent dup { model: opus }
|
|
1471
1744
|
agent dup { model: sonnet }
|
|
@@ -1479,7 +1752,10 @@ describe("Validation errors", () => {
|
|
|
1479
1752
|
});
|
|
1480
1753
|
it("V3 FlowReferences: flow edge referencing a non-existent node", () => {
|
|
1481
1754
|
const src = `topology t : [pipeline] {
|
|
1482
|
-
orchestrator {
|
|
1755
|
+
orchestrator {
|
|
1756
|
+
model: opus
|
|
1757
|
+
handles: [a]
|
|
1758
|
+
}
|
|
1483
1759
|
action a { kind: inline }
|
|
1484
1760
|
agent real { model: opus }
|
|
1485
1761
|
flow { a -> ghost }
|
|
@@ -1492,7 +1768,10 @@ describe("Validation errors", () => {
|
|
|
1492
1768
|
});
|
|
1493
1769
|
it("V14 ToolExclusivity: agent with both tools and disallowed-tools", () => {
|
|
1494
1770
|
const src = `topology t : [pipeline] {
|
|
1495
|
-
orchestrator {
|
|
1771
|
+
orchestrator {
|
|
1772
|
+
model: opus
|
|
1773
|
+
handles: [a]
|
|
1774
|
+
}
|
|
1496
1775
|
action a { kind: inline }
|
|
1497
1776
|
agent confused {
|
|
1498
1777
|
model: opus
|
|
@@ -1509,7 +1788,10 @@ describe("Validation errors", () => {
|
|
|
1509
1788
|
});
|
|
1510
1789
|
it("V6 LoopBound: back-edge without max N annotation", () => {
|
|
1511
1790
|
const src = `topology t : [pipeline] {
|
|
1512
|
-
orchestrator {
|
|
1791
|
+
orchestrator {
|
|
1792
|
+
model: opus
|
|
1793
|
+
handles: [a]
|
|
1794
|
+
}
|
|
1513
1795
|
action a { kind: inline }
|
|
1514
1796
|
agent step1 { model: opus }
|
|
1515
1797
|
agent step2 { model: opus }
|
|
@@ -1527,7 +1809,10 @@ describe("Validation errors", () => {
|
|
|
1527
1809
|
});
|
|
1528
1810
|
it("V6 NoSelfEdges: self-referencing edge without max bound", () => {
|
|
1529
1811
|
const src = `topology t : [pipeline] {
|
|
1530
|
-
orchestrator {
|
|
1812
|
+
orchestrator {
|
|
1813
|
+
model: opus
|
|
1814
|
+
handles: [a]
|
|
1815
|
+
}
|
|
1531
1816
|
action a { kind: inline }
|
|
1532
1817
|
agent analyzer { model: opus }
|
|
1533
1818
|
flow {
|
|
@@ -1543,7 +1828,10 @@ describe("Validation errors", () => {
|
|
|
1543
1828
|
});
|
|
1544
1829
|
it("V13 GateAnchors: gate with after referencing non-existent node", () => {
|
|
1545
1830
|
const src = `topology t : [pipeline] {
|
|
1546
|
-
orchestrator {
|
|
1831
|
+
orchestrator {
|
|
1832
|
+
model: opus
|
|
1833
|
+
handles: [a]
|
|
1834
|
+
}
|
|
1547
1835
|
action a { kind: inline }
|
|
1548
1836
|
agent worker { model: opus }
|
|
1549
1837
|
gates {
|
|
@@ -1567,7 +1855,10 @@ describe("Validation errors", () => {
|
|
|
1567
1855
|
describe("Validation warnings", () => {
|
|
1568
1856
|
it("V18 ModelNotInProvider: agent uses model not listed in any provider", () => {
|
|
1569
1857
|
const src = `topology t : [pipeline] {
|
|
1570
|
-
orchestrator {
|
|
1858
|
+
orchestrator {
|
|
1859
|
+
model: opus
|
|
1860
|
+
handles: [a]
|
|
1861
|
+
}
|
|
1571
1862
|
action a { kind: inline }
|
|
1572
1863
|
agent worker {
|
|
1573
1864
|
model: mystery-model
|
|
@@ -1588,7 +1879,10 @@ describe("Validation warnings", () => {
|
|
|
1588
1879
|
});
|
|
1589
1880
|
it("V22 FallbackNotInProvider: fallback chain model not in provider", () => {
|
|
1590
1881
|
const src = `topology t : [pipeline] {
|
|
1591
|
-
orchestrator {
|
|
1882
|
+
orchestrator {
|
|
1883
|
+
model: opus
|
|
1884
|
+
handles: [a]
|
|
1885
|
+
}
|
|
1592
1886
|
action a { kind: inline }
|
|
1593
1887
|
agent worker {
|
|
1594
1888
|
model: opus
|
|
@@ -1615,7 +1909,10 @@ describe("Validation warnings", () => {
|
|
|
1615
1909
|
describe("Edge cases that should NOT throw", () => {
|
|
1616
1910
|
it("agent with no tools parses fine", () => {
|
|
1617
1911
|
const src = `topology t : [pipeline] {
|
|
1618
|
-
orchestrator {
|
|
1912
|
+
orchestrator {
|
|
1913
|
+
model: opus
|
|
1914
|
+
handles: [a]
|
|
1915
|
+
}
|
|
1619
1916
|
action a { kind: inline }
|
|
1620
1917
|
agent bare { model: opus }
|
|
1621
1918
|
flow { a -> bare }
|
|
@@ -1627,7 +1924,10 @@ describe("Edge cases that should NOT throw", () => {
|
|
|
1627
1924
|
});
|
|
1628
1925
|
it("agent with empty prompt block", () => {
|
|
1629
1926
|
const src = `topology t : [pipeline] {
|
|
1630
|
-
orchestrator {
|
|
1927
|
+
orchestrator {
|
|
1928
|
+
model: opus
|
|
1929
|
+
handles: [a]
|
|
1930
|
+
}
|
|
1631
1931
|
action a { kind: inline }
|
|
1632
1932
|
agent empty-prompt {
|
|
1633
1933
|
model: opus
|
|
@@ -1642,7 +1942,10 @@ describe("Edge cases that should NOT throw", () => {
|
|
|
1642
1942
|
});
|
|
1643
1943
|
it("flow with no conditions (all unconditional edges)", () => {
|
|
1644
1944
|
const src = `topology t : [pipeline] {
|
|
1645
|
-
orchestrator {
|
|
1945
|
+
orchestrator {
|
|
1946
|
+
model: opus
|
|
1947
|
+
handles: [a]
|
|
1948
|
+
}
|
|
1646
1949
|
action a { kind: inline }
|
|
1647
1950
|
agent s1 { model: opus }
|
|
1648
1951
|
agent s2 { model: opus }
|
|
@@ -1661,7 +1964,10 @@ describe("Edge cases that should NOT throw", () => {
|
|
|
1661
1964
|
});
|
|
1662
1965
|
it("topology with no agents (just an orchestrator)", () => {
|
|
1663
1966
|
const src = `topology t : [pipeline] {
|
|
1664
|
-
orchestrator {
|
|
1967
|
+
orchestrator {
|
|
1968
|
+
model: opus
|
|
1969
|
+
handles: [a]
|
|
1970
|
+
}
|
|
1665
1971
|
action a { kind: inline }
|
|
1666
1972
|
flow { a -> a }
|
|
1667
1973
|
}`;
|
|
@@ -1741,7 +2047,10 @@ topology test-v12-invalid : [pipeline] {
|
|
|
1741
2047
|
it("[max 3] alone (no when) should produce no V12 error", () => {
|
|
1742
2048
|
const src = `
|
|
1743
2049
|
topology test-v12-maxonly : [pipeline] {
|
|
1744
|
-
orchestrator {
|
|
2050
|
+
orchestrator {
|
|
2051
|
+
model: sonnet
|
|
2052
|
+
handles: [intake]
|
|
2053
|
+
}
|
|
1745
2054
|
action intake {}
|
|
1746
2055
|
agent worker { model: sonnet }
|
|
1747
2056
|
flow {
|
|
@@ -1776,7 +2085,10 @@ topology test-v12-whenonly : [pipeline] {
|
|
|
1776
2085
|
describe("Flow parsing edge cases (Phase 2)", () => {
|
|
1777
2086
|
it("chain with 3+ nodes: a -> b -> c -> d produces 3 edges", () => {
|
|
1778
2087
|
const src = `topology t : [pipeline] {
|
|
1779
|
-
orchestrator {
|
|
2088
|
+
orchestrator {
|
|
2089
|
+
model: opus
|
|
2090
|
+
handles: [a]
|
|
2091
|
+
}
|
|
1780
2092
|
action a { kind: inline }
|
|
1781
2093
|
agent b { model: opus }
|
|
1782
2094
|
agent c { model: opus }
|
|
@@ -1815,7 +2127,10 @@ describe("Flow parsing edge cases (Phase 2)", () => {
|
|
|
1815
2127
|
});
|
|
1816
2128
|
it("empty flow block produces empty edges array", () => {
|
|
1817
2129
|
const src = `topology t : [pipeline] {
|
|
1818
|
-
orchestrator {
|
|
2130
|
+
orchestrator {
|
|
2131
|
+
model: opus
|
|
2132
|
+
handles: [a]
|
|
2133
|
+
}
|
|
1819
2134
|
action a { kind: inline }
|
|
1820
2135
|
flow {
|
|
1821
2136
|
}
|
|
@@ -1825,9 +2140,15 @@ describe("Flow parsing edge cases (Phase 2)", () => {
|
|
|
1825
2140
|
});
|
|
1826
2141
|
it("single node (no arrow) produces no edges", () => {
|
|
1827
2142
|
const src = `topology t : [pipeline] {
|
|
1828
|
-
orchestrator {
|
|
2143
|
+
orchestrator {
|
|
2144
|
+
model: opus
|
|
2145
|
+
handles: [a]
|
|
2146
|
+
}
|
|
1829
2147
|
action a { kind: inline }
|
|
1830
|
-
agent lonely {
|
|
2148
|
+
agent lonely {
|
|
2149
|
+
model: opus
|
|
2150
|
+
invocation: manual
|
|
2151
|
+
}
|
|
1831
2152
|
flow {
|
|
1832
2153
|
}
|
|
1833
2154
|
}`;
|
|
@@ -1841,7 +2162,10 @@ describe("Role prefix matching (Phase 2)", () => {
|
|
|
1841
2162
|
roles {
|
|
1842
2163
|
review: "Performs reviews"
|
|
1843
2164
|
}
|
|
1844
|
-
orchestrator {
|
|
2165
|
+
orchestrator {
|
|
2166
|
+
model: opus
|
|
2167
|
+
handles: [a]
|
|
2168
|
+
}
|
|
1845
2169
|
action a { kind: inline }
|
|
1846
2170
|
agent review-security {
|
|
1847
2171
|
model: opus
|
|
@@ -1859,7 +2183,10 @@ describe("Role prefix matching (Phase 2)", () => {
|
|
|
1859
2183
|
review: "Generic reviewer"
|
|
1860
2184
|
review-security: "Security reviewer"
|
|
1861
2185
|
}
|
|
1862
|
-
orchestrator {
|
|
2186
|
+
orchestrator {
|
|
2187
|
+
model: opus
|
|
2188
|
+
handles: [a]
|
|
2189
|
+
}
|
|
1863
2190
|
action a { kind: inline }
|
|
1864
2191
|
agent review-security {
|
|
1865
2192
|
model: opus
|
|
@@ -1876,7 +2203,10 @@ describe("Role prefix matching (Phase 2)", () => {
|
|
|
1876
2203
|
roles {
|
|
1877
2204
|
writer: "Writes content"
|
|
1878
2205
|
}
|
|
1879
|
-
orchestrator {
|
|
2206
|
+
orchestrator {
|
|
2207
|
+
model: opus
|
|
2208
|
+
handles: [a]
|
|
2209
|
+
}
|
|
1880
2210
|
action a { kind: inline }
|
|
1881
2211
|
agent analyzer {
|
|
1882
2212
|
model: opus
|
|
@@ -1961,7 +2291,10 @@ describe("Line number tracking", () => {
|
|
|
1961
2291
|
// Gate after: a human node — cannot be wired as a SubagentStop hook
|
|
1962
2292
|
// because human nodes do not register as subagent_types.
|
|
1963
2293
|
const ast = parse(`topology t : [pipeline] {
|
|
1964
|
-
orchestrator {
|
|
2294
|
+
orchestrator {
|
|
2295
|
+
model: opus
|
|
2296
|
+
handles: [a]
|
|
2297
|
+
}
|
|
1965
2298
|
action a { kind: inline }
|
|
1966
2299
|
agent b { model: opus }
|
|
1967
2300
|
human review-step {
|
|
@@ -1989,7 +2322,10 @@ describe("Line number tracking", () => {
|
|
|
1989
2322
|
// Gate after: an agent node — compiles to a SubagentStop hook that
|
|
1990
2323
|
// blocks (exit 2 prevents subagent from stopping). No warning needed.
|
|
1991
2324
|
const ast = parse(`topology t : [pipeline] {
|
|
1992
|
-
orchestrator {
|
|
2325
|
+
orchestrator {
|
|
2326
|
+
model: opus
|
|
2327
|
+
handles: [a]
|
|
2328
|
+
}
|
|
1993
2329
|
action a { kind: inline }
|
|
1994
2330
|
agent b { model: opus }
|
|
1995
2331
|
agent c { model: opus }
|
|
@@ -2056,7 +2392,10 @@ describe("Line number tracking", () => {
|
|
|
2056
2392
|
});
|
|
2057
2393
|
it("V87: orchestrator with no delegation field is accepted (defaults to subagent)", () => {
|
|
2058
2394
|
const ast = parse(`topology t : [pipeline] {
|
|
2059
|
-
orchestrator {
|
|
2395
|
+
orchestrator {
|
|
2396
|
+
model: opus
|
|
2397
|
+
handles: [a]
|
|
2398
|
+
}
|
|
2060
2399
|
action a { kind: inline }
|
|
2061
2400
|
flow { a -> a }
|
|
2062
2401
|
}`);
|
|
@@ -2071,7 +2410,10 @@ describe("Line number tracking", () => {
|
|
|
2071
2410
|
describe("C12: env sub-map inside mcp-servers entries", () => {
|
|
2072
2411
|
it("parses env block inside an mcp-server entry", () => {
|
|
2073
2412
|
const src = `topology t : [pipeline] {
|
|
2074
|
-
orchestrator {
|
|
2413
|
+
orchestrator {
|
|
2414
|
+
model: opus
|
|
2415
|
+
handles: [a]
|
|
2416
|
+
}
|
|
2075
2417
|
action a { kind: inline }
|
|
2076
2418
|
agent w { model: opus }
|
|
2077
2419
|
mcp-servers {
|
|
@@ -2097,7 +2439,10 @@ describe("C12: env sub-map inside mcp-servers entries", () => {
|
|
|
2097
2439
|
});
|
|
2098
2440
|
it("mcp-server without env block still works", () => {
|
|
2099
2441
|
const src = `topology t : [pipeline] {
|
|
2100
|
-
orchestrator {
|
|
2442
|
+
orchestrator {
|
|
2443
|
+
model: opus
|
|
2444
|
+
handles: [a]
|
|
2445
|
+
}
|
|
2101
2446
|
action a { kind: inline }
|
|
2102
2447
|
agent w { model: opus }
|
|
2103
2448
|
mcp-servers {
|
|
@@ -2115,7 +2460,10 @@ describe("C12: env sub-map inside mcp-servers entries", () => {
|
|
|
2115
2460
|
});
|
|
2116
2461
|
it("parses multiple mcp-servers with env blocks", () => {
|
|
2117
2462
|
const src = `topology t : [pipeline] {
|
|
2118
|
-
orchestrator {
|
|
2463
|
+
orchestrator {
|
|
2464
|
+
model: opus
|
|
2465
|
+
handles: [a]
|
|
2466
|
+
}
|
|
2119
2467
|
action a { kind: inline }
|
|
2120
2468
|
agent w { model: opus }
|
|
2121
2469
|
mcp-servers {
|
|
@@ -2142,7 +2490,10 @@ describe("C12: env sub-map inside mcp-servers entries", () => {
|
|
|
2142
2490
|
describe("C13: extensions block inside hook bodies", () => {
|
|
2143
2491
|
it("parses extensions inside a global hook", () => {
|
|
2144
2492
|
const src = `topology t : [pipeline] {
|
|
2145
|
-
orchestrator {
|
|
2493
|
+
orchestrator {
|
|
2494
|
+
model: opus
|
|
2495
|
+
handles: [a]
|
|
2496
|
+
}
|
|
2146
2497
|
action a { kind: inline }
|
|
2147
2498
|
agent w { model: opus }
|
|
2148
2499
|
hooks {
|
|
@@ -2168,7 +2519,10 @@ describe("C13: extensions block inside hook bodies", () => {
|
|
|
2168
2519
|
});
|
|
2169
2520
|
it("parses extensions inside a per-agent hook", () => {
|
|
2170
2521
|
const src = `topology t : [pipeline] {
|
|
2171
|
-
orchestrator {
|
|
2522
|
+
orchestrator {
|
|
2523
|
+
model: opus
|
|
2524
|
+
handles: [a]
|
|
2525
|
+
}
|
|
2172
2526
|
action a { kind: inline }
|
|
2173
2527
|
agent w {
|
|
2174
2528
|
model: opus
|
|
@@ -2195,7 +2549,10 @@ describe("C13: extensions block inside hook bodies", () => {
|
|
|
2195
2549
|
});
|
|
2196
2550
|
it("hook without extensions still works", () => {
|
|
2197
2551
|
const src = `topology t : [pipeline] {
|
|
2198
|
-
orchestrator {
|
|
2552
|
+
orchestrator {
|
|
2553
|
+
model: opus
|
|
2554
|
+
handles: [a]
|
|
2555
|
+
}
|
|
2199
2556
|
action a { kind: inline }
|
|
2200
2557
|
agent w { model: opus }
|
|
2201
2558
|
hooks {
|
|
@@ -2215,7 +2572,10 @@ describe("C13: extensions block inside hook bodies", () => {
|
|
|
2215
2572
|
describe("C14: top-level extensions block in topology body", () => {
|
|
2216
2573
|
it("parses a top-level extensions block", () => {
|
|
2217
2574
|
const src = `topology t : [pipeline] {
|
|
2218
|
-
orchestrator {
|
|
2575
|
+
orchestrator {
|
|
2576
|
+
model: opus
|
|
2577
|
+
handles: [a]
|
|
2578
|
+
}
|
|
2219
2579
|
action a { kind: inline }
|
|
2220
2580
|
agent w { model: opus }
|
|
2221
2581
|
extensions {
|
|
@@ -2241,7 +2601,10 @@ describe("C14: top-level extensions block in topology body", () => {
|
|
|
2241
2601
|
});
|
|
2242
2602
|
it("topology without extensions block has no extensions field", () => {
|
|
2243
2603
|
const src = `topology t : [pipeline] {
|
|
2244
|
-
orchestrator {
|
|
2604
|
+
orchestrator {
|
|
2605
|
+
model: opus
|
|
2606
|
+
handles: [a]
|
|
2607
|
+
}
|
|
2245
2608
|
action a { kind: inline }
|
|
2246
2609
|
agent w { model: opus }
|
|
2247
2610
|
flow { a -> w }
|
|
@@ -2257,7 +2620,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2257
2620
|
describe("Parsing", () => {
|
|
2258
2621
|
it("agent with timeout parses correctly", () => {
|
|
2259
2622
|
const src = `topology t : [pipeline] {
|
|
2260
|
-
orchestrator {
|
|
2623
|
+
orchestrator {
|
|
2624
|
+
model: opus
|
|
2625
|
+
handles: [a]
|
|
2626
|
+
}
|
|
2261
2627
|
action a { kind: inline }
|
|
2262
2628
|
agent w {
|
|
2263
2629
|
model: sonnet
|
|
@@ -2271,7 +2637,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2271
2637
|
});
|
|
2272
2638
|
it("agent with on-fail: halt parses correctly", () => {
|
|
2273
2639
|
const src = `topology t : [pipeline] {
|
|
2274
|
-
orchestrator {
|
|
2640
|
+
orchestrator {
|
|
2641
|
+
model: opus
|
|
2642
|
+
handles: [a]
|
|
2643
|
+
}
|
|
2275
2644
|
action a { kind: inline }
|
|
2276
2645
|
agent w {
|
|
2277
2646
|
model: sonnet
|
|
@@ -2285,7 +2654,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2285
2654
|
});
|
|
2286
2655
|
it("agent with on-fail: retry parses correctly", () => {
|
|
2287
2656
|
const src = `topology t : [pipeline] {
|
|
2288
|
-
orchestrator {
|
|
2657
|
+
orchestrator {
|
|
2658
|
+
model: opus
|
|
2659
|
+
handles: [a]
|
|
2660
|
+
}
|
|
2289
2661
|
action a { kind: inline }
|
|
2290
2662
|
agent w {
|
|
2291
2663
|
model: sonnet
|
|
@@ -2299,7 +2671,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2299
2671
|
});
|
|
2300
2672
|
it("agent with on-fail: skip parses correctly", () => {
|
|
2301
2673
|
const src = `topology t : [pipeline] {
|
|
2302
|
-
orchestrator {
|
|
2674
|
+
orchestrator {
|
|
2675
|
+
model: opus
|
|
2676
|
+
handles: [a]
|
|
2677
|
+
}
|
|
2303
2678
|
action a { kind: inline }
|
|
2304
2679
|
agent w {
|
|
2305
2680
|
model: sonnet
|
|
@@ -2313,7 +2688,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2313
2688
|
});
|
|
2314
2689
|
it("agent with on-fail: fallback parses correctly", () => {
|
|
2315
2690
|
const src = `topology t : [pipeline] {
|
|
2316
|
-
orchestrator {
|
|
2691
|
+
orchestrator {
|
|
2692
|
+
model: opus
|
|
2693
|
+
handles: [a]
|
|
2694
|
+
}
|
|
2317
2695
|
action a { kind: inline }
|
|
2318
2696
|
agent w {
|
|
2319
2697
|
model: sonnet
|
|
@@ -2330,7 +2708,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2330
2708
|
});
|
|
2331
2709
|
it("simple retry: 3 still works (backward compat)", () => {
|
|
2332
2710
|
const src = `topology t : [pipeline] {
|
|
2333
|
-
orchestrator {
|
|
2711
|
+
orchestrator {
|
|
2712
|
+
model: opus
|
|
2713
|
+
handles: [a]
|
|
2714
|
+
}
|
|
2334
2715
|
action a { kind: inline }
|
|
2335
2716
|
agent w {
|
|
2336
2717
|
model: sonnet
|
|
@@ -2344,7 +2725,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2344
2725
|
});
|
|
2345
2726
|
it("retry block parses all fields", () => {
|
|
2346
2727
|
const src = `topology t : [pipeline] {
|
|
2347
|
-
orchestrator {
|
|
2728
|
+
orchestrator {
|
|
2729
|
+
model: opus
|
|
2730
|
+
handles: [a]
|
|
2731
|
+
}
|
|
2348
2732
|
action a { kind: inline }
|
|
2349
2733
|
agent w {
|
|
2350
2734
|
model: sonnet
|
|
@@ -2372,7 +2756,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2372
2756
|
});
|
|
2373
2757
|
it("action with timeout and on-fail parses correctly", () => {
|
|
2374
2758
|
const src = `topology t : [pipeline] {
|
|
2375
|
-
orchestrator {
|
|
2759
|
+
orchestrator {
|
|
2760
|
+
model: opus
|
|
2761
|
+
handles: [a]
|
|
2762
|
+
}
|
|
2376
2763
|
action a {
|
|
2377
2764
|
kind: inline
|
|
2378
2765
|
timeout: 30s
|
|
@@ -2389,7 +2776,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2389
2776
|
});
|
|
2390
2777
|
it("gate with timeout parses correctly", () => {
|
|
2391
2778
|
const src = `topology t : [pipeline] {
|
|
2392
|
-
orchestrator {
|
|
2779
|
+
orchestrator {
|
|
2780
|
+
model: opus
|
|
2781
|
+
handles: [a]
|
|
2782
|
+
}
|
|
2393
2783
|
action a { kind: inline }
|
|
2394
2784
|
agent w { model: sonnet }
|
|
2395
2785
|
gates {
|
|
@@ -2560,7 +2950,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2560
2950
|
nodes: [
|
|
2561
2951
|
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
2562
2952
|
{ id: "intake", type: "action", label: "I" },
|
|
2563
|
-
{ id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
|
|
2953
|
+
{ id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
|
|
2954
|
+
max: 3,
|
|
2955
|
+
backoff: "random"
|
|
2956
|
+
} },
|
|
2564
2957
|
],
|
|
2565
2958
|
});
|
|
2566
2959
|
const results = validate(ast);
|
|
@@ -2573,7 +2966,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2573
2966
|
nodes: [
|
|
2574
2967
|
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
2575
2968
|
{ id: "intake", type: "action", label: "I" },
|
|
2576
|
-
{ id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
|
|
2969
|
+
{ id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
|
|
2970
|
+
max: 3,
|
|
2971
|
+
interval: "forever"
|
|
2972
|
+
} },
|
|
2577
2973
|
],
|
|
2578
2974
|
});
|
|
2579
2975
|
const results = validate(ast);
|
|
@@ -2586,7 +2982,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2586
2982
|
nodes: [
|
|
2587
2983
|
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
2588
2984
|
{ id: "intake", type: "action", label: "I" },
|
|
2589
|
-
{ id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
|
|
2985
|
+
{ id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
|
|
2986
|
+
max: 3,
|
|
2987
|
+
maxInterval: "nope"
|
|
2988
|
+
} },
|
|
2590
2989
|
],
|
|
2591
2990
|
});
|
|
2592
2991
|
const results = validate(ast);
|
|
@@ -2599,7 +2998,14 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
|
|
|
2599
2998
|
nodes: [
|
|
2600
2999
|
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
2601
3000
|
{ id: "intake", type: "action", label: "I" },
|
|
2602
|
-
{ id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
|
|
3001
|
+
{ id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
|
|
3002
|
+
max: 3,
|
|
3003
|
+
backoff: "exponential",
|
|
3004
|
+
interval: "1s",
|
|
3005
|
+
maxInterval: "60s",
|
|
3006
|
+
jitter: true,
|
|
3007
|
+
nonRetryable: ["auth-error"]
|
|
3008
|
+
} },
|
|
2603
3009
|
],
|
|
2604
3010
|
});
|
|
2605
3011
|
const results = validate(ast);
|
|
@@ -2904,7 +3310,10 @@ topology code-review : [pipeline] {
|
|
|
2904
3310
|
describe("Wave 2: Error edges (-x->)", () => {
|
|
2905
3311
|
it("parses -x-> as an error edge with isError: true", () => {
|
|
2906
3312
|
const src = `topology t : [pipeline] {
|
|
2907
|
-
orchestrator {
|
|
3313
|
+
orchestrator {
|
|
3314
|
+
model: opus
|
|
3315
|
+
handles: [a]
|
|
3316
|
+
}
|
|
2908
3317
|
action a { kind: inline }
|
|
2909
3318
|
agent analyzer { model: opus }
|
|
2910
3319
|
agent error-handler { model: opus }
|
|
@@ -2921,7 +3330,10 @@ describe("Wave 2: Error edges (-x->)", () => {
|
|
|
2921
3330
|
});
|
|
2922
3331
|
it("parses -x[timeout]-> as a typed error edge", () => {
|
|
2923
3332
|
const src = `topology t : [pipeline] {
|
|
2924
|
-
orchestrator {
|
|
3333
|
+
orchestrator {
|
|
3334
|
+
model: opus
|
|
3335
|
+
handles: [a]
|
|
3336
|
+
}
|
|
2925
3337
|
action a { kind: inline }
|
|
2926
3338
|
agent analyzer { model: opus }
|
|
2927
3339
|
agent fallback { model: opus }
|
|
@@ -2938,7 +3350,10 @@ describe("Wave 2: Error edges (-x->)", () => {
|
|
|
2938
3350
|
});
|
|
2939
3351
|
it("parses -x[auth-error]-> with hyphenated error type", () => {
|
|
2940
3352
|
const src = `topology t : [pipeline] {
|
|
2941
|
-
orchestrator {
|
|
3353
|
+
orchestrator {
|
|
3354
|
+
model: opus
|
|
3355
|
+
handles: [a]
|
|
3356
|
+
}
|
|
2942
3357
|
action a { kind: inline }
|
|
2943
3358
|
agent caller { model: opus }
|
|
2944
3359
|
agent auth-fallback { model: opus }
|
|
@@ -2955,7 +3370,10 @@ describe("Wave 2: Error edges (-x->)", () => {
|
|
|
2955
3370
|
});
|
|
2956
3371
|
it("normal edges do not have isError set", () => {
|
|
2957
3372
|
const src = `topology t : [pipeline] {
|
|
2958
|
-
orchestrator {
|
|
3373
|
+
orchestrator {
|
|
3374
|
+
model: opus
|
|
3375
|
+
handles: [a]
|
|
3376
|
+
}
|
|
2959
3377
|
action a { kind: inline }
|
|
2960
3378
|
agent b { model: opus }
|
|
2961
3379
|
flow {
|
|
@@ -2969,7 +3387,10 @@ describe("Wave 2: Error edges (-x->)", () => {
|
|
|
2969
3387
|
});
|
|
2970
3388
|
it("mixed normal and error edges on same node", () => {
|
|
2971
3389
|
const src = `topology t : [pipeline] {
|
|
2972
|
-
orchestrator {
|
|
3390
|
+
orchestrator {
|
|
3391
|
+
model: opus
|
|
3392
|
+
handles: [a]
|
|
3393
|
+
}
|
|
2973
3394
|
action a { kind: inline }
|
|
2974
3395
|
agent analyzer { model: opus }
|
|
2975
3396
|
agent reviewer { model: opus }
|
|
@@ -2992,7 +3413,10 @@ describe("Wave 2: Error edges (-x->)", () => {
|
|
|
2992
3413
|
describe("Wave 2: [tolerance] edge attribute", () => {
|
|
2993
3414
|
it("parses [tolerance: 2] as integer tolerance", () => {
|
|
2994
3415
|
const src = `topology t : [pipeline] {
|
|
2995
|
-
orchestrator {
|
|
3416
|
+
orchestrator {
|
|
3417
|
+
model: opus
|
|
3418
|
+
handles: [a]
|
|
3419
|
+
}
|
|
2996
3420
|
action a { kind: inline }
|
|
2997
3421
|
agent b { model: opus }
|
|
2998
3422
|
agent c { model: opus }
|
|
@@ -3010,7 +3434,10 @@ describe("Wave 2: [tolerance] edge attribute", () => {
|
|
|
3010
3434
|
});
|
|
3011
3435
|
it("parses [tolerance: 33%] as percentage string", () => {
|
|
3012
3436
|
const src = `topology t : [pipeline] {
|
|
3013
|
-
orchestrator {
|
|
3437
|
+
orchestrator {
|
|
3438
|
+
model: opus
|
|
3439
|
+
handles: [a]
|
|
3440
|
+
}
|
|
3014
3441
|
action a { kind: inline }
|
|
3015
3442
|
agent b { model: opus }
|
|
3016
3443
|
agent c { model: opus }
|
|
@@ -3029,7 +3456,10 @@ describe("Wave 2: [tolerance] edge attribute", () => {
|
|
|
3029
3456
|
describe("Wave 2: [race] edge attribute", () => {
|
|
3030
3457
|
it("parses [race] as boolean edge attribute", () => {
|
|
3031
3458
|
const src = `topology t : [pipeline] {
|
|
3032
|
-
orchestrator {
|
|
3459
|
+
orchestrator {
|
|
3460
|
+
model: opus
|
|
3461
|
+
handles: [a]
|
|
3462
|
+
}
|
|
3033
3463
|
action a { kind: inline }
|
|
3034
3464
|
agent fast { model: opus }
|
|
3035
3465
|
agent slow { model: opus }
|
|
@@ -3048,7 +3478,10 @@ describe("Wave 2: [race] edge attribute", () => {
|
|
|
3048
3478
|
describe("Wave 2: [wait N] edge attribute", () => {
|
|
3049
3479
|
it("parses [wait 30s] as inline timer", () => {
|
|
3050
3480
|
const src = `topology t : [pipeline] {
|
|
3051
|
-
orchestrator {
|
|
3481
|
+
orchestrator {
|
|
3482
|
+
model: opus
|
|
3483
|
+
handles: [a]
|
|
3484
|
+
}
|
|
3052
3485
|
action a { kind: inline }
|
|
3053
3486
|
agent notifier { model: opus }
|
|
3054
3487
|
flow {
|
|
@@ -3062,7 +3495,10 @@ describe("Wave 2: [wait N] edge attribute", () => {
|
|
|
3062
3495
|
});
|
|
3063
3496
|
it("parses [wait 5m] with minute duration", () => {
|
|
3064
3497
|
const src = `topology t : [pipeline] {
|
|
3065
|
-
orchestrator {
|
|
3498
|
+
orchestrator {
|
|
3499
|
+
model: opus
|
|
3500
|
+
handles: [a]
|
|
3501
|
+
}
|
|
3066
3502
|
action a { kind: inline }
|
|
3067
3503
|
agent notifier { model: opus }
|
|
3068
3504
|
flow {
|
|
@@ -3078,7 +3514,10 @@ describe("Wave 2: [wait N] edge attribute", () => {
|
|
|
3078
3514
|
describe("Wave 2: join field on agent/action", () => {
|
|
3079
3515
|
it("parses join: all on agent", () => {
|
|
3080
3516
|
const src = `topology t : [pipeline] {
|
|
3081
|
-
orchestrator {
|
|
3517
|
+
orchestrator {
|
|
3518
|
+
model: opus
|
|
3519
|
+
handles: [a]
|
|
3520
|
+
}
|
|
3082
3521
|
action a { kind: inline }
|
|
3083
3522
|
agent synthesizer {
|
|
3084
3523
|
model: opus
|
|
@@ -3092,7 +3531,10 @@ describe("Wave 2: join field on agent/action", () => {
|
|
|
3092
3531
|
});
|
|
3093
3532
|
it("parses join: any on agent", () => {
|
|
3094
3533
|
const src = `topology t : [pipeline] {
|
|
3095
|
-
orchestrator {
|
|
3534
|
+
orchestrator {
|
|
3535
|
+
model: opus
|
|
3536
|
+
handles: [a]
|
|
3537
|
+
}
|
|
3096
3538
|
action a { kind: inline }
|
|
3097
3539
|
agent fast-consumer {
|
|
3098
3540
|
model: opus
|
|
@@ -3106,7 +3548,10 @@ describe("Wave 2: join field on agent/action", () => {
|
|
|
3106
3548
|
});
|
|
3107
3549
|
it("parses join: all-done on agent", () => {
|
|
3108
3550
|
const src = `topology t : [pipeline] {
|
|
3109
|
-
orchestrator {
|
|
3551
|
+
orchestrator {
|
|
3552
|
+
model: opus
|
|
3553
|
+
handles: [a]
|
|
3554
|
+
}
|
|
3110
3555
|
action a { kind: inline }
|
|
3111
3556
|
agent collector {
|
|
3112
3557
|
model: opus
|
|
@@ -3120,7 +3565,10 @@ describe("Wave 2: join field on agent/action", () => {
|
|
|
3120
3565
|
});
|
|
3121
3566
|
it("parses join on action", () => {
|
|
3122
3567
|
const src = `topology t : [pipeline] {
|
|
3123
|
-
orchestrator {
|
|
3568
|
+
orchestrator {
|
|
3569
|
+
model: opus
|
|
3570
|
+
handles: [merge-step]
|
|
3571
|
+
}
|
|
3124
3572
|
action merge-step {
|
|
3125
3573
|
kind: inline
|
|
3126
3574
|
join: none-failed
|
|
@@ -3140,7 +3588,10 @@ describe("Wave 2: Topology meta timeout and error-handler", () => {
|
|
|
3140
3588
|
version: "1.0.0"
|
|
3141
3589
|
timeout: 30m
|
|
3142
3590
|
}
|
|
3143
|
-
orchestrator {
|
|
3591
|
+
orchestrator {
|
|
3592
|
+
model: opus
|
|
3593
|
+
handles: [a]
|
|
3594
|
+
}
|
|
3144
3595
|
action a { kind: inline }
|
|
3145
3596
|
flow { a -> a }
|
|
3146
3597
|
}`;
|
|
@@ -3153,7 +3604,10 @@ describe("Wave 2: Topology meta timeout and error-handler", () => {
|
|
|
3153
3604
|
version: "1.0.0"
|
|
3154
3605
|
error-handler: global-error-agent
|
|
3155
3606
|
}
|
|
3156
|
-
orchestrator {
|
|
3607
|
+
orchestrator {
|
|
3608
|
+
model: opus
|
|
3609
|
+
handles: [a]
|
|
3610
|
+
}
|
|
3157
3611
|
action a { kind: inline }
|
|
3158
3612
|
agent global-error-agent { model: opus }
|
|
3159
3613
|
flow { a -> global-error-agent }
|
|
@@ -3168,7 +3622,10 @@ describe("Wave 2: Topology meta timeout and error-handler", () => {
|
|
|
3168
3622
|
timeout: 2h
|
|
3169
3623
|
error-handler: catch-all
|
|
3170
3624
|
}
|
|
3171
|
-
orchestrator {
|
|
3625
|
+
orchestrator {
|
|
3626
|
+
model: opus
|
|
3627
|
+
handles: [a]
|
|
3628
|
+
}
|
|
3172
3629
|
action a { kind: inline }
|
|
3173
3630
|
agent catch-all { model: opus }
|
|
3174
3631
|
flow { a -> catch-all }
|
|
@@ -3409,7 +3866,10 @@ describe("Wave 2: Validator rules V39-V45", () => {
|
|
|
3409
3866
|
describe("Wave 2: Backward compatibility", () => {
|
|
3410
3867
|
it("existing flow syntax with [when] and [max] still works", () => {
|
|
3411
3868
|
const src = `topology t : [pipeline] {
|
|
3412
|
-
orchestrator {
|
|
3869
|
+
orchestrator {
|
|
3870
|
+
model: opus
|
|
3871
|
+
handles: [a]
|
|
3872
|
+
}
|
|
3413
3873
|
action a { kind: inline }
|
|
3414
3874
|
agent b { model: opus
|
|
3415
3875
|
outputs { verdict: yes | no } }
|
|
@@ -3429,7 +3889,10 @@ describe("Wave 2: Backward compatibility", () => {
|
|
|
3429
3889
|
});
|
|
3430
3890
|
it("existing fan-out syntax still works", () => {
|
|
3431
3891
|
const src = `topology t : [pipeline] {
|
|
3432
|
-
orchestrator {
|
|
3892
|
+
orchestrator {
|
|
3893
|
+
model: opus
|
|
3894
|
+
handles: [start]
|
|
3895
|
+
}
|
|
3433
3896
|
action start { kind: inline }
|
|
3434
3897
|
agent x { model: opus }
|
|
3435
3898
|
agent y { model: opus }
|
|
@@ -3446,7 +3909,10 @@ describe("Wave 2: Backward compatibility", () => {
|
|
|
3446
3909
|
});
|
|
3447
3910
|
it("existing chain syntax a -> b -> c still works", () => {
|
|
3448
3911
|
const src = `topology t : [pipeline] {
|
|
3449
|
-
orchestrator {
|
|
3912
|
+
orchestrator {
|
|
3913
|
+
model: opus
|
|
3914
|
+
handles: [a]
|
|
3915
|
+
}
|
|
3450
3916
|
action a { kind: inline }
|
|
3451
3917
|
agent b { model: opus }
|
|
3452
3918
|
agent c { model: opus }
|
|
@@ -3461,7 +3927,10 @@ describe("Wave 2: Backward compatibility", () => {
|
|
|
3461
3927
|
});
|
|
3462
3928
|
it("existing [per id] syntax still works", () => {
|
|
3463
3929
|
const src = `topology t : [pipeline] {
|
|
3464
|
-
orchestrator {
|
|
3930
|
+
orchestrator {
|
|
3931
|
+
model: opus
|
|
3932
|
+
handles: [a]
|
|
3933
|
+
}
|
|
3465
3934
|
action a { kind: inline }
|
|
3466
3935
|
agent b { model: opus }
|
|
3467
3936
|
flow {
|
|
@@ -3475,7 +3944,10 @@ describe("Wave 2: Backward compatibility", () => {
|
|
|
3475
3944
|
});
|
|
3476
3945
|
it("existing [when ..., max N] combined syntax still works", () => {
|
|
3477
3946
|
const src = `topology t : [pipeline] {
|
|
3478
|
-
orchestrator {
|
|
3947
|
+
orchestrator {
|
|
3948
|
+
model: opus
|
|
3949
|
+
handles: [a]
|
|
3950
|
+
}
|
|
3479
3951
|
action a { kind: inline }
|
|
3480
3952
|
agent b { model: opus
|
|
3481
3953
|
outputs { status: pass | fail } }
|
|
@@ -3881,10 +4353,13 @@ topology test : [pipeline] {
|
|
|
3881
4353
|
// =========================================================================
|
|
3882
4354
|
describe("Sensitive and Secret env values", () => {
|
|
3883
4355
|
const minimalPrefix = `topology t : [pipeline] {
|
|
3884
|
-
orchestrator {
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
4356
|
+
orchestrator {
|
|
4357
|
+
model: opus
|
|
4358
|
+
handles: [a]
|
|
4359
|
+
}
|
|
4360
|
+
action a { kind: inline }
|
|
4361
|
+
flow { a -> a }`;
|
|
4362
|
+
it("parses sensitive env var reference as SensitiveValue", () => {
|
|
3888
4363
|
const src = `${minimalPrefix}
|
|
3889
4364
|
env {
|
|
3890
4365
|
API_KEY: sensitive "\${API_KEY}"
|
|
@@ -3958,7 +4433,10 @@ describe("Sensitive and Secret env values", () => {
|
|
|
3958
4433
|
});
|
|
3959
4434
|
describe("V51 - Sensitive literal warning", () => {
|
|
3960
4435
|
const minimalPrefix = `topology t : [pipeline] {
|
|
3961
|
-
orchestrator {
|
|
4436
|
+
orchestrator {
|
|
4437
|
+
model: opus
|
|
4438
|
+
handles: [a]
|
|
4439
|
+
}
|
|
3962
4440
|
action a { kind: inline }
|
|
3963
4441
|
flow { a -> a }`;
|
|
3964
4442
|
it("warns when sensitive is used with a literal string", () => {
|
|
@@ -4010,7 +4488,10 @@ describe("V51 - Sensitive literal warning", () => {
|
|
|
4010
4488
|
});
|
|
4011
4489
|
describe("V52 - Secret URI scheme validation", () => {
|
|
4012
4490
|
const minimalPrefix = `topology t : [pipeline] {
|
|
4013
|
-
orchestrator {
|
|
4491
|
+
orchestrator {
|
|
4492
|
+
model: opus
|
|
4493
|
+
handles: [a]
|
|
4494
|
+
}
|
|
4014
4495
|
action a { kind: inline }
|
|
4015
4496
|
flow { a -> a }`;
|
|
4016
4497
|
it("errors on unknown secret URI scheme", () => {
|
|
@@ -4058,7 +4539,10 @@ describe("V52 - Secret URI scheme validation", () => {
|
|
|
4058
4539
|
describe("Wave 3: Observability block parsing", () => {
|
|
4059
4540
|
it("full observability block parses correctly", () => {
|
|
4060
4541
|
const src = `topology t : [pipeline] {
|
|
4061
|
-
orchestrator {
|
|
4542
|
+
orchestrator {
|
|
4543
|
+
model: opus
|
|
4544
|
+
handles: [a]
|
|
4545
|
+
}
|
|
4062
4546
|
action a { kind: inline }
|
|
4063
4547
|
flow { a -> a }
|
|
4064
4548
|
observability {
|
|
@@ -4101,7 +4585,10 @@ describe("Wave 3: Observability block parsing", () => {
|
|
|
4101
4585
|
});
|
|
4102
4586
|
it("observability with only capture sub-block", () => {
|
|
4103
4587
|
const src = `topology t : [pipeline] {
|
|
4104
|
-
orchestrator {
|
|
4588
|
+
orchestrator {
|
|
4589
|
+
model: opus
|
|
4590
|
+
handles: [a]
|
|
4591
|
+
}
|
|
4105
4592
|
action a { kind: inline }
|
|
4106
4593
|
flow { a -> a }
|
|
4107
4594
|
observability {
|
|
@@ -4129,7 +4616,10 @@ describe("Wave 3: Observability block parsing", () => {
|
|
|
4129
4616
|
});
|
|
4130
4617
|
it("observability with only spans sub-block", () => {
|
|
4131
4618
|
const src = `topology t : [pipeline] {
|
|
4132
|
-
orchestrator {
|
|
4619
|
+
orchestrator {
|
|
4620
|
+
model: opus
|
|
4621
|
+
handles: [a]
|
|
4622
|
+
}
|
|
4133
4623
|
action a { kind: inline }
|
|
4134
4624
|
flow { a -> a }
|
|
4135
4625
|
observability {
|
|
@@ -4154,7 +4644,10 @@ describe("Wave 3: Observability block parsing", () => {
|
|
|
4154
4644
|
});
|
|
4155
4645
|
it("defaults are applied when fields omitted", () => {
|
|
4156
4646
|
const src = `topology t : [pipeline] {
|
|
4157
|
-
orchestrator {
|
|
4647
|
+
orchestrator {
|
|
4648
|
+
model: opus
|
|
4649
|
+
handles: [a]
|
|
4650
|
+
}
|
|
4158
4651
|
action a { kind: inline }
|
|
4159
4652
|
flow { a -> a }
|
|
4160
4653
|
observability {
|
|
@@ -4179,7 +4672,10 @@ describe("Wave 3: Observability block parsing", () => {
|
|
|
4179
4672
|
});
|
|
4180
4673
|
it("backward compat: topology without observability -> null", () => {
|
|
4181
4674
|
const src = `topology t : [pipeline] {
|
|
4182
|
-
orchestrator {
|
|
4675
|
+
orchestrator {
|
|
4676
|
+
model: opus
|
|
4677
|
+
handles: [a]
|
|
4678
|
+
}
|
|
4183
4679
|
action a { kind: inline }
|
|
4184
4680
|
flow { a -> a }
|
|
4185
4681
|
}`;
|
|
@@ -4188,7 +4684,10 @@ describe("Wave 3: Observability block parsing", () => {
|
|
|
4188
4684
|
});
|
|
4189
4685
|
it("observability with enabled: false", () => {
|
|
4190
4686
|
const src = `topology t : [pipeline] {
|
|
4191
|
-
orchestrator {
|
|
4687
|
+
orchestrator {
|
|
4688
|
+
model: opus
|
|
4689
|
+
handles: [a]
|
|
4690
|
+
}
|
|
4192
4691
|
action a { kind: inline }
|
|
4193
4692
|
flow { a -> a }
|
|
4194
4693
|
observability {
|
|
@@ -4849,7 +5348,10 @@ topology simple : [pipeline] {
|
|
|
4849
5348
|
describe("Wave 5 — Compensation / Saga (F27)", () => {
|
|
4850
5349
|
it("parses agent with compensates field", () => {
|
|
4851
5350
|
const src = `topology t : [pipeline] {
|
|
4852
|
-
orchestrator {
|
|
5351
|
+
orchestrator {
|
|
5352
|
+
model: opus
|
|
5353
|
+
handles: [a]
|
|
5354
|
+
}
|
|
4853
5355
|
action a { kind: inline }
|
|
4854
5356
|
agent deployer {
|
|
4855
5357
|
model: sonnet
|
|
@@ -4867,7 +5369,10 @@ describe("Wave 5 — Compensation / Saga (F27)", () => {
|
|
|
4867
5369
|
});
|
|
4868
5370
|
it("agent without compensates has no compensates field", () => {
|
|
4869
5371
|
const src = `topology t : [pipeline] {
|
|
4870
|
-
orchestrator {
|
|
5372
|
+
orchestrator {
|
|
5373
|
+
model: opus
|
|
5374
|
+
handles: [a]
|
|
5375
|
+
}
|
|
4871
5376
|
action a { kind: inline }
|
|
4872
5377
|
agent worker {
|
|
4873
5378
|
model: sonnet
|
|
@@ -4881,7 +5386,10 @@ describe("Wave 5 — Compensation / Saga (F27)", () => {
|
|
|
4881
5386
|
});
|
|
4882
5387
|
it("V58: compensates referencing non-existent agent -> error", () => {
|
|
4883
5388
|
const src = `topology t : [pipeline] {
|
|
4884
|
-
orchestrator {
|
|
5389
|
+
orchestrator {
|
|
5390
|
+
model: opus
|
|
5391
|
+
handles: [a]
|
|
5392
|
+
}
|
|
4885
5393
|
action a { kind: inline }
|
|
4886
5394
|
agent rollback {
|
|
4887
5395
|
model: sonnet
|
|
@@ -4899,7 +5407,10 @@ describe("Wave 5 — Compensation / Saga (F27)", () => {
|
|
|
4899
5407
|
});
|
|
4900
5408
|
it("V58: compensates referencing existing agent -> no error", () => {
|
|
4901
5409
|
const src = `topology t : [pipeline] {
|
|
4902
|
-
orchestrator {
|
|
5410
|
+
orchestrator {
|
|
5411
|
+
model: opus
|
|
5412
|
+
handles: [a]
|
|
5413
|
+
}
|
|
4903
5414
|
action a { kind: inline }
|
|
4904
5415
|
agent deployer {
|
|
4905
5416
|
model: sonnet
|
|
@@ -4917,7 +5428,10 @@ describe("Wave 5 — Compensation / Saga (F27)", () => {
|
|
|
4917
5428
|
});
|
|
4918
5429
|
it("backward compat: existing topologies without compensates still work", () => {
|
|
4919
5430
|
const src = `topology t : [pipeline] {
|
|
4920
|
-
orchestrator {
|
|
5431
|
+
orchestrator {
|
|
5432
|
+
model: opus
|
|
5433
|
+
handles: [a]
|
|
5434
|
+
}
|
|
4921
5435
|
action a { kind: inline }
|
|
4922
5436
|
agent w1 { model: sonnet }
|
|
4923
5437
|
agent w2 { model: opus }
|
|
@@ -4942,7 +5456,10 @@ describe("Circuit Breaker (F26)", () => {
|
|
|
4942
5456
|
describe("Parsing", () => {
|
|
4943
5457
|
it("agent with circuit-breaker block parses correctly", () => {
|
|
4944
5458
|
const src = `topology t : [pipeline] {
|
|
4945
|
-
orchestrator {
|
|
5459
|
+
orchestrator {
|
|
5460
|
+
model: opus
|
|
5461
|
+
handles: [a]
|
|
5462
|
+
}
|
|
4946
5463
|
action a { kind: inline }
|
|
4947
5464
|
agent w {
|
|
4948
5465
|
model: sonnet
|
|
@@ -4963,7 +5480,10 @@ describe("Circuit Breaker (F26)", () => {
|
|
|
4963
5480
|
});
|
|
4964
5481
|
it("agent without circuit-breaker has no circuitBreaker field", () => {
|
|
4965
5482
|
const src = `topology t : [pipeline] {
|
|
4966
|
-
orchestrator {
|
|
5483
|
+
orchestrator {
|
|
5484
|
+
model: opus
|
|
5485
|
+
handles: [a]
|
|
5486
|
+
}
|
|
4967
5487
|
action a { kind: inline }
|
|
4968
5488
|
agent w { model: sonnet }
|
|
4969
5489
|
flow { a -> w }
|
|
@@ -5021,7 +5541,11 @@ describe("Circuit Breaker (F26)", () => {
|
|
|
5021
5541
|
nodes: [
|
|
5022
5542
|
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
5023
5543
|
{ id: "intake", type: "action", label: "Intake" },
|
|
5024
|
-
{ id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
|
|
5544
|
+
{ id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
|
|
5545
|
+
threshold: 5,
|
|
5546
|
+
window: "5m",
|
|
5547
|
+
cooldown: "30s"
|
|
5548
|
+
} },
|
|
5025
5549
|
],
|
|
5026
5550
|
});
|
|
5027
5551
|
const results = validate(ast);
|
|
@@ -5033,7 +5557,11 @@ describe("Circuit Breaker (F26)", () => {
|
|
|
5033
5557
|
nodes: [
|
|
5034
5558
|
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
5035
5559
|
{ id: "intake", type: "action", label: "Intake" },
|
|
5036
|
-
{ id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
|
|
5560
|
+
{ id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
|
|
5561
|
+
threshold: 0,
|
|
5562
|
+
window: "5m",
|
|
5563
|
+
cooldown: "30s"
|
|
5564
|
+
} },
|
|
5037
5565
|
],
|
|
5038
5566
|
});
|
|
5039
5567
|
const results = validate(ast);
|
|
@@ -5046,7 +5574,11 @@ describe("Circuit Breaker (F26)", () => {
|
|
|
5046
5574
|
nodes: [
|
|
5047
5575
|
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
5048
5576
|
{ id: "intake", type: "action", label: "Intake" },
|
|
5049
|
-
{ id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
|
|
5577
|
+
{ id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
|
|
5578
|
+
threshold: 3,
|
|
5579
|
+
window: "five-min",
|
|
5580
|
+
cooldown: "30s"
|
|
5581
|
+
} },
|
|
5050
5582
|
],
|
|
5051
5583
|
});
|
|
5052
5584
|
const results = validate(ast);
|
|
@@ -5059,7 +5591,11 @@ describe("Circuit Breaker (F26)", () => {
|
|
|
5059
5591
|
nodes: [
|
|
5060
5592
|
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
5061
5593
|
{ id: "intake", type: "action", label: "Intake" },
|
|
5062
|
-
{ id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
|
|
5594
|
+
{ id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
|
|
5595
|
+
threshold: 3,
|
|
5596
|
+
window: "5m",
|
|
5597
|
+
cooldown: "invalid"
|
|
5598
|
+
} },
|
|
5063
5599
|
],
|
|
5064
5600
|
});
|
|
5065
5601
|
const results = validate(ast);
|
|
@@ -5072,7 +5608,11 @@ describe("Circuit Breaker (F26)", () => {
|
|
|
5072
5608
|
nodes: [
|
|
5073
5609
|
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
5074
5610
|
{ id: "intake", type: "action", label: "Intake" },
|
|
5075
|
-
{ id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
|
|
5611
|
+
{ id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
|
|
5612
|
+
threshold: -1,
|
|
5613
|
+
window: "bad",
|
|
5614
|
+
cooldown: "worse"
|
|
5615
|
+
} },
|
|
5076
5616
|
],
|
|
5077
5617
|
});
|
|
5078
5618
|
const results = validate(ast);
|
|
@@ -5093,7 +5633,10 @@ describe("Circuit Breaker (F26)", () => {
|
|
|
5093
5633
|
describe("Human node type (F32)", () => {
|
|
5094
5634
|
it("parses human node with all fields", () => {
|
|
5095
5635
|
const src = `topology t : [pipeline] {
|
|
5096
|
-
orchestrator {
|
|
5636
|
+
orchestrator {
|
|
5637
|
+
model: opus
|
|
5638
|
+
handles: [intake]
|
|
5639
|
+
}
|
|
5097
5640
|
action intake { kind: inline }
|
|
5098
5641
|
human approval {
|
|
5099
5642
|
description: "Human reviews and approves the deployment plan"
|
|
@@ -5113,7 +5656,10 @@ describe("Human node type (F32)", () => {
|
|
|
5113
5656
|
});
|
|
5114
5657
|
it("human node appears in flow edges", () => {
|
|
5115
5658
|
const src = `topology t : [pipeline] {
|
|
5116
|
-
orchestrator {
|
|
5659
|
+
orchestrator {
|
|
5660
|
+
model: opus
|
|
5661
|
+
handles: [intake]
|
|
5662
|
+
}
|
|
5117
5663
|
action intake { kind: inline }
|
|
5118
5664
|
human review {
|
|
5119
5665
|
description: "Review step"
|
|
@@ -5128,7 +5674,10 @@ describe("Human node type (F32)", () => {
|
|
|
5128
5674
|
});
|
|
5129
5675
|
it("V59: invalid on-timeout -> error", () => {
|
|
5130
5676
|
const src = `topology t : [pipeline] {
|
|
5131
|
-
orchestrator {
|
|
5677
|
+
orchestrator {
|
|
5678
|
+
model: opus
|
|
5679
|
+
handles: [intake]
|
|
5680
|
+
}
|
|
5132
5681
|
action intake { kind: inline }
|
|
5133
5682
|
human approval {
|
|
5134
5683
|
timeout: 1h
|
|
@@ -5147,7 +5696,10 @@ describe("Human node type (F32)", () => {
|
|
|
5147
5696
|
it("V59: valid on-timeout values pass", () => {
|
|
5148
5697
|
for (const value of ["halt", "skip", "fallback backup-agent"]) {
|
|
5149
5698
|
const src = `topology t : [pipeline] {
|
|
5150
|
-
orchestrator {
|
|
5699
|
+
orchestrator {
|
|
5700
|
+
model: opus
|
|
5701
|
+
handles: [intake]
|
|
5702
|
+
}
|
|
5151
5703
|
action intake { kind: inline }
|
|
5152
5704
|
human approval {
|
|
5153
5705
|
timeout: 1h
|
|
@@ -5163,7 +5715,10 @@ describe("Human node type (F32)", () => {
|
|
|
5163
5715
|
});
|
|
5164
5716
|
it("V30: invalid timeout on human node -> error", () => {
|
|
5165
5717
|
const src = `topology t : [pipeline] {
|
|
5166
|
-
orchestrator {
|
|
5718
|
+
orchestrator {
|
|
5719
|
+
model: opus
|
|
5720
|
+
handles: [intake]
|
|
5721
|
+
}
|
|
5167
5722
|
action intake { kind: inline }
|
|
5168
5723
|
human approval {
|
|
5169
5724
|
timeout: forever
|
|
@@ -5179,7 +5734,10 @@ describe("Human node type (F32)", () => {
|
|
|
5179
5734
|
});
|
|
5180
5735
|
it("backward compat: existing topologies without human nodes still work", () => {
|
|
5181
5736
|
const src = `topology t : [pipeline] {
|
|
5182
|
-
orchestrator {
|
|
5737
|
+
orchestrator {
|
|
5738
|
+
model: opus
|
|
5739
|
+
handles: [a]
|
|
5740
|
+
}
|
|
5183
5741
|
action a { kind: inline }
|
|
5184
5742
|
agent w1 { model: sonnet }
|
|
5185
5743
|
agent w2 { model: opus }
|
|
@@ -5200,7 +5758,10 @@ describe("Human node type (F32)", () => {
|
|
|
5200
5758
|
describe("F31: Quorum join (N-of-M)", () => {
|
|
5201
5759
|
it("parses join: 2-of-3 on agent", () => {
|
|
5202
5760
|
const src = `topology t : [pipeline] {
|
|
5203
|
-
orchestrator {
|
|
5761
|
+
orchestrator {
|
|
5762
|
+
model: opus
|
|
5763
|
+
handles: [a]
|
|
5764
|
+
}
|
|
5204
5765
|
action a { kind: inline }
|
|
5205
5766
|
agent synthesizer {
|
|
5206
5767
|
model: opus
|
|
@@ -5214,7 +5775,10 @@ describe("F31: Quorum join (N-of-M)", () => {
|
|
|
5214
5775
|
});
|
|
5215
5776
|
it("parses join: 3-of-5 on agent", () => {
|
|
5216
5777
|
const src = `topology t : [pipeline] {
|
|
5217
|
-
orchestrator {
|
|
5778
|
+
orchestrator {
|
|
5779
|
+
model: opus
|
|
5780
|
+
handles: [a]
|
|
5781
|
+
}
|
|
5218
5782
|
action a { kind: inline }
|
|
5219
5783
|
agent voter {
|
|
5220
5784
|
model: opus
|
|
@@ -5285,7 +5849,10 @@ describe("F31: Quorum join (N-of-M)", () => {
|
|
|
5285
5849
|
describe("F35: Weighted routing [weight N]", () => {
|
|
5286
5850
|
it("parses [weight 0.7] as edge attribute", () => {
|
|
5287
5851
|
const src = `topology t : [pipeline] {
|
|
5288
|
-
orchestrator {
|
|
5852
|
+
orchestrator {
|
|
5853
|
+
model: opus
|
|
5854
|
+
handles: [a]
|
|
5855
|
+
}
|
|
5289
5856
|
action a { kind: inline }
|
|
5290
5857
|
agent b { model: opus }
|
|
5291
5858
|
flow {
|
|
@@ -5299,7 +5866,10 @@ describe("F35: Weighted routing [weight N]", () => {
|
|
|
5299
5866
|
});
|
|
5300
5867
|
it("parses [weight 1.0] as edge attribute", () => {
|
|
5301
5868
|
const src = `topology t : [pipeline] {
|
|
5302
|
-
orchestrator {
|
|
5869
|
+
orchestrator {
|
|
5870
|
+
model: opus
|
|
5871
|
+
handles: [a]
|
|
5872
|
+
}
|
|
5303
5873
|
action a { kind: inline }
|
|
5304
5874
|
agent b { model: opus }
|
|
5305
5875
|
flow {
|
|
@@ -5313,7 +5883,10 @@ describe("F35: Weighted routing [weight N]", () => {
|
|
|
5313
5883
|
});
|
|
5314
5884
|
it("parses [weight N] on fan-out edges", () => {
|
|
5315
5885
|
const src = `topology t : [pipeline] {
|
|
5316
|
-
orchestrator {
|
|
5886
|
+
orchestrator {
|
|
5887
|
+
model: opus
|
|
5888
|
+
handles: [router]
|
|
5889
|
+
}
|
|
5317
5890
|
action router { kind: inline }
|
|
5318
5891
|
agent a { model: opus }
|
|
5319
5892
|
agent b { model: opus }
|
|
@@ -5447,7 +6020,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
|
|
|
5447
6020
|
ttl: 7d
|
|
5448
6021
|
}
|
|
5449
6022
|
|
|
5450
|
-
orchestrator {
|
|
6023
|
+
orchestrator {
|
|
6024
|
+
model: opus
|
|
6025
|
+
handles: [intake]
|
|
6026
|
+
}
|
|
5451
6027
|
action intake { kind: inline }
|
|
5452
6028
|
agent worker { model: sonnet }
|
|
5453
6029
|
flow { intake -> worker }
|
|
@@ -5466,7 +6042,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
|
|
|
5466
6042
|
version: "1.0.0"
|
|
5467
6043
|
durable: true
|
|
5468
6044
|
}
|
|
5469
|
-
orchestrator {
|
|
6045
|
+
orchestrator {
|
|
6046
|
+
model: opus
|
|
6047
|
+
handles: [intake]
|
|
6048
|
+
}
|
|
5470
6049
|
action intake { kind: inline }
|
|
5471
6050
|
agent worker { model: sonnet }
|
|
5472
6051
|
flow { intake -> worker }
|
|
@@ -5477,7 +6056,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
|
|
|
5477
6056
|
it("durable defaults to undefined when not set", () => {
|
|
5478
6057
|
const src = `topology t : [pipeline] {
|
|
5479
6058
|
meta { version: "1.0.0" }
|
|
5480
|
-
orchestrator {
|
|
6059
|
+
orchestrator {
|
|
6060
|
+
model: opus
|
|
6061
|
+
handles: [intake]
|
|
6062
|
+
}
|
|
5481
6063
|
action intake { kind: inline }
|
|
5482
6064
|
agent worker { model: sonnet }
|
|
5483
6065
|
flow { intake -> worker }
|
|
@@ -5488,7 +6070,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
|
|
|
5488
6070
|
it("checkpoint defaults to null when not present", () => {
|
|
5489
6071
|
const src = `topology t : [pipeline] {
|
|
5490
6072
|
meta { version: "1.0.0" }
|
|
5491
|
-
orchestrator {
|
|
6073
|
+
orchestrator {
|
|
6074
|
+
model: opus
|
|
6075
|
+
handles: [intake]
|
|
6076
|
+
}
|
|
5492
6077
|
action intake { kind: inline }
|
|
5493
6078
|
agent worker { model: sonnet }
|
|
5494
6079
|
flow { intake -> worker }
|
|
@@ -5502,7 +6087,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
|
|
|
5502
6087
|
backend: mongodb
|
|
5503
6088
|
strategy: every-node
|
|
5504
6089
|
}
|
|
5505
|
-
orchestrator {
|
|
6090
|
+
orchestrator {
|
|
6091
|
+
model: opus
|
|
6092
|
+
handles: [intake]
|
|
6093
|
+
}
|
|
5506
6094
|
action intake { kind: inline }
|
|
5507
6095
|
agent worker { model: sonnet }
|
|
5508
6096
|
flow { intake -> worker }
|
|
@@ -5521,7 +6109,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
|
|
|
5521
6109
|
backend: ${backend}
|
|
5522
6110
|
strategy: every-node
|
|
5523
6111
|
}
|
|
5524
|
-
orchestrator {
|
|
6112
|
+
orchestrator {
|
|
6113
|
+
model: opus
|
|
6114
|
+
handles: [intake]
|
|
6115
|
+
}
|
|
5525
6116
|
action intake { kind: inline }
|
|
5526
6117
|
agent worker { model: sonnet }
|
|
5527
6118
|
flow { intake -> worker }
|
|
@@ -5538,7 +6129,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
|
|
|
5538
6129
|
backend: postgres
|
|
5539
6130
|
strategy: periodic
|
|
5540
6131
|
}
|
|
5541
|
-
orchestrator {
|
|
6132
|
+
orchestrator {
|
|
6133
|
+
model: opus
|
|
6134
|
+
handles: [intake]
|
|
6135
|
+
}
|
|
5542
6136
|
action intake { kind: inline }
|
|
5543
6137
|
agent worker { model: sonnet }
|
|
5544
6138
|
flow { intake -> worker }
|
|
@@ -5557,7 +6151,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
|
|
|
5557
6151
|
strategy: every-node
|
|
5558
6152
|
ttl: forever
|
|
5559
6153
|
}
|
|
5560
|
-
orchestrator {
|
|
6154
|
+
orchestrator {
|
|
6155
|
+
model: opus
|
|
6156
|
+
handles: [intake]
|
|
6157
|
+
}
|
|
5561
6158
|
action intake { kind: inline }
|
|
5562
6159
|
agent worker { model: sonnet }
|
|
5563
6160
|
flow { intake -> worker }
|
|
@@ -5575,7 +6172,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
|
|
|
5575
6172
|
strategy: every-node
|
|
5576
6173
|
ttl: 7d
|
|
5577
6174
|
}
|
|
5578
|
-
orchestrator {
|
|
6175
|
+
orchestrator {
|
|
6176
|
+
model: opus
|
|
6177
|
+
handles: [intake]
|
|
6178
|
+
}
|
|
5579
6179
|
action intake { kind: inline }
|
|
5580
6180
|
agent worker { model: sonnet }
|
|
5581
6181
|
flow { intake -> worker }
|
|
@@ -5601,7 +6201,10 @@ describe("Time-Travel / Replay (F37)", () => {
|
|
|
5601
6201
|
branch: true
|
|
5602
6202
|
}
|
|
5603
6203
|
}
|
|
5604
|
-
orchestrator {
|
|
6204
|
+
orchestrator {
|
|
6205
|
+
model: opus
|
|
6206
|
+
handles: [intake]
|
|
6207
|
+
}
|
|
5605
6208
|
action intake { kind: inline }
|
|
5606
6209
|
agent worker { model: sonnet }
|
|
5607
6210
|
flow { intake -> worker }
|
|
@@ -5622,7 +6225,10 @@ describe("Time-Travel / Replay (F37)", () => {
|
|
|
5622
6225
|
enabled: true
|
|
5623
6226
|
}
|
|
5624
6227
|
}
|
|
5625
|
-
orchestrator {
|
|
6228
|
+
orchestrator {
|
|
6229
|
+
model: opus
|
|
6230
|
+
handles: [intake]
|
|
6231
|
+
}
|
|
5626
6232
|
action intake { kind: inline }
|
|
5627
6233
|
agent worker { model: sonnet }
|
|
5628
6234
|
flow { intake -> worker }
|
|
@@ -5641,7 +6247,10 @@ describe("Time-Travel / Replay (F37)", () => {
|
|
|
5641
6247
|
enabled: true
|
|
5642
6248
|
}
|
|
5643
6249
|
}
|
|
5644
|
-
orchestrator {
|
|
6250
|
+
orchestrator {
|
|
6251
|
+
model: opus
|
|
6252
|
+
handles: [intake]
|
|
6253
|
+
}
|
|
5645
6254
|
action intake { kind: inline }
|
|
5646
6255
|
agent worker { model: sonnet }
|
|
5647
6256
|
flow { intake -> worker }
|
|
@@ -5661,7 +6270,10 @@ describe("Time-Travel / Replay (F37)", () => {
|
|
|
5661
6270
|
max-history: -5
|
|
5662
6271
|
}
|
|
5663
6272
|
}
|
|
5664
|
-
orchestrator {
|
|
6273
|
+
orchestrator {
|
|
6274
|
+
model: opus
|
|
6275
|
+
handles: [intake]
|
|
6276
|
+
}
|
|
5665
6277
|
action intake { kind: inline }
|
|
5666
6278
|
agent worker { model: sonnet }
|
|
5667
6279
|
flow { intake -> worker }
|
|
@@ -5680,7 +6292,10 @@ describe("Time-Travel / Replay (F37)", () => {
|
|
|
5680
6292
|
enabled: true
|
|
5681
6293
|
}
|
|
5682
6294
|
}
|
|
5683
|
-
orchestrator {
|
|
6295
|
+
orchestrator {
|
|
6296
|
+
model: opus
|
|
6297
|
+
handles: [intake]
|
|
6298
|
+
}
|
|
5684
6299
|
action intake { kind: inline }
|
|
5685
6300
|
agent worker { model: sonnet }
|
|
5686
6301
|
flow { intake -> worker }
|
|
@@ -5710,7 +6325,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
|
|
|
5710
6325
|
depends-on: [research-notes]
|
|
5711
6326
|
}
|
|
5712
6327
|
}
|
|
5713
|
-
orchestrator {
|
|
6328
|
+
orchestrator {
|
|
6329
|
+
model: opus
|
|
6330
|
+
handles: [intake]
|
|
6331
|
+
}
|
|
5714
6332
|
action intake { kind: inline }
|
|
5715
6333
|
agent researcher {
|
|
5716
6334
|
model: sonnet
|
|
@@ -5740,7 +6358,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
|
|
|
5740
6358
|
});
|
|
5741
6359
|
it("empty artifacts block returns empty array", () => {
|
|
5742
6360
|
const src = `topology t : [pipeline] {
|
|
5743
|
-
orchestrator {
|
|
6361
|
+
orchestrator {
|
|
6362
|
+
model: opus
|
|
6363
|
+
handles: [intake]
|
|
6364
|
+
}
|
|
5744
6365
|
action intake { kind: inline }
|
|
5745
6366
|
agent worker { model: sonnet }
|
|
5746
6367
|
flow { intake -> worker }
|
|
@@ -5758,7 +6379,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
|
|
|
5758
6379
|
type: json
|
|
5759
6380
|
}
|
|
5760
6381
|
}
|
|
5761
|
-
orchestrator {
|
|
6382
|
+
orchestrator {
|
|
6383
|
+
model: opus
|
|
6384
|
+
handles: [intake]
|
|
6385
|
+
}
|
|
5762
6386
|
action intake { kind: inline }
|
|
5763
6387
|
agent worker { model: sonnet }
|
|
5764
6388
|
flow { intake -> worker }
|
|
@@ -5777,7 +6401,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
|
|
|
5777
6401
|
type: markdown
|
|
5778
6402
|
}
|
|
5779
6403
|
}
|
|
5780
|
-
orchestrator {
|
|
6404
|
+
orchestrator {
|
|
6405
|
+
model: opus
|
|
6406
|
+
handles: [intake]
|
|
6407
|
+
}
|
|
5781
6408
|
action intake { kind: inline }
|
|
5782
6409
|
agent worker {
|
|
5783
6410
|
model: sonnet
|
|
@@ -5799,7 +6426,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
|
|
|
5799
6426
|
type: markdown
|
|
5800
6427
|
}
|
|
5801
6428
|
}
|
|
5802
|
-
orchestrator {
|
|
6429
|
+
orchestrator {
|
|
6430
|
+
model: opus
|
|
6431
|
+
handles: [intake]
|
|
6432
|
+
}
|
|
5803
6433
|
action intake { kind: inline }
|
|
5804
6434
|
agent worker {
|
|
5805
6435
|
model: sonnet
|
|
@@ -5821,7 +6451,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
|
|
|
5821
6451
|
depends-on: [phantom]
|
|
5822
6452
|
}
|
|
5823
6453
|
}
|
|
5824
|
-
orchestrator {
|
|
6454
|
+
orchestrator {
|
|
6455
|
+
model: opus
|
|
6456
|
+
handles: [intake]
|
|
6457
|
+
}
|
|
5825
6458
|
action intake { kind: inline }
|
|
5826
6459
|
agent worker { model: sonnet }
|
|
5827
6460
|
flow { intake -> worker }
|
|
@@ -5843,7 +6476,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
|
|
|
5843
6476
|
depends-on: [notes]
|
|
5844
6477
|
}
|
|
5845
6478
|
}
|
|
5846
|
-
orchestrator {
|
|
6479
|
+
orchestrator {
|
|
6480
|
+
model: opus
|
|
6481
|
+
handles: [intake]
|
|
6482
|
+
}
|
|
5847
6483
|
action intake { kind: inline }
|
|
5848
6484
|
agent writer {
|
|
5849
6485
|
model: sonnet
|
|
@@ -5869,7 +6505,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
|
|
|
5869
6505
|
depends-on: [a]
|
|
5870
6506
|
}
|
|
5871
6507
|
}
|
|
5872
|
-
orchestrator {
|
|
6508
|
+
orchestrator {
|
|
6509
|
+
model: opus
|
|
6510
|
+
handles: [intake]
|
|
6511
|
+
}
|
|
5873
6512
|
action intake { kind: inline }
|
|
5874
6513
|
agent worker { model: sonnet }
|
|
5875
6514
|
flow { intake -> worker }
|
|
@@ -5896,7 +6535,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
|
|
|
5896
6535
|
depends-on: [clean]
|
|
5897
6536
|
}
|
|
5898
6537
|
}
|
|
5899
|
-
orchestrator {
|
|
6538
|
+
orchestrator {
|
|
6539
|
+
model: opus
|
|
6540
|
+
handles: [intake]
|
|
6541
|
+
}
|
|
5900
6542
|
action intake { kind: inline }
|
|
5901
6543
|
agent worker { model: sonnet }
|
|
5902
6544
|
flow { intake -> worker }
|
|
@@ -5913,7 +6555,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
|
|
|
5913
6555
|
describe("Reflection edge attribute (F33)", () => {
|
|
5914
6556
|
it("parses [reflection] attribute — sets reflection to true", () => {
|
|
5915
6557
|
const src = `topology t : [pipeline] {
|
|
5916
|
-
orchestrator {
|
|
6558
|
+
orchestrator {
|
|
6559
|
+
model: opus
|
|
6560
|
+
handles: [intake]
|
|
6561
|
+
}
|
|
5917
6562
|
action intake { kind: inline }
|
|
5918
6563
|
agent writer { model: sonnet }
|
|
5919
6564
|
agent evaluator { model: opus }
|
|
@@ -5931,7 +6576,10 @@ describe("Reflection edge attribute (F33)", () => {
|
|
|
5931
6576
|
});
|
|
5932
6577
|
it("V62: [reflection] without [max N] — error", () => {
|
|
5933
6578
|
const src = `topology t : [pipeline] {
|
|
5934
|
-
orchestrator {
|
|
6579
|
+
orchestrator {
|
|
6580
|
+
model: opus
|
|
6581
|
+
handles: [intake]
|
|
6582
|
+
}
|
|
5935
6583
|
action intake { kind: inline }
|
|
5936
6584
|
agent writer { model: sonnet }
|
|
5937
6585
|
agent evaluator { model: opus }
|
|
@@ -5951,7 +6599,10 @@ describe("Reflection edge attribute (F33)", () => {
|
|
|
5951
6599
|
});
|
|
5952
6600
|
it("V62: [reflection] on non-back-edge — warning", () => {
|
|
5953
6601
|
const src = `topology t : [pipeline] {
|
|
5954
|
-
orchestrator {
|
|
6602
|
+
orchestrator {
|
|
6603
|
+
model: opus
|
|
6604
|
+
handles: [intake]
|
|
6605
|
+
}
|
|
5955
6606
|
action intake { kind: inline }
|
|
5956
6607
|
agent writer { model: sonnet }
|
|
5957
6608
|
agent evaluator { model: opus }
|
|
@@ -5972,7 +6623,10 @@ describe("Reflection edge attribute (F33)", () => {
|
|
|
5972
6623
|
describe("Group chat node (F36)", () => {
|
|
5973
6624
|
it("parses group node with all fields", () => {
|
|
5974
6625
|
const src = `topology t : [pipeline] {
|
|
5975
|
-
orchestrator {
|
|
6626
|
+
orchestrator {
|
|
6627
|
+
model: opus
|
|
6628
|
+
handles: [intake]
|
|
6629
|
+
}
|
|
5976
6630
|
action intake { kind: inline }
|
|
5977
6631
|
agent architect { model: opus }
|
|
5978
6632
|
agent critic { model: sonnet }
|
|
@@ -6000,7 +6654,10 @@ describe("Group chat node (F36)", () => {
|
|
|
6000
6654
|
});
|
|
6001
6655
|
it("group node appears in flow edges", () => {
|
|
6002
6656
|
const src = `topology t : [pipeline] {
|
|
6003
|
-
orchestrator {
|
|
6657
|
+
orchestrator {
|
|
6658
|
+
model: opus
|
|
6659
|
+
handles: [intake]
|
|
6660
|
+
}
|
|
6004
6661
|
action intake { kind: inline }
|
|
6005
6662
|
agent a1 { model: sonnet }
|
|
6006
6663
|
agent a2 { model: sonnet }
|
|
@@ -6017,7 +6674,10 @@ describe("Group chat node (F36)", () => {
|
|
|
6017
6674
|
});
|
|
6018
6675
|
it("V63: members reference non-existent agents — error", () => {
|
|
6019
6676
|
const src = `topology t : [pipeline] {
|
|
6020
|
-
orchestrator {
|
|
6677
|
+
orchestrator {
|
|
6678
|
+
model: opus
|
|
6679
|
+
handles: [intake]
|
|
6680
|
+
}
|
|
6021
6681
|
action intake { kind: inline }
|
|
6022
6682
|
agent a1 { model: sonnet }
|
|
6023
6683
|
group chat {
|
|
@@ -6034,7 +6694,10 @@ describe("Group chat node (F36)", () => {
|
|
|
6034
6694
|
});
|
|
6035
6695
|
it("V64: invalid speaker-selection — error", () => {
|
|
6036
6696
|
const src = `topology t : [pipeline] {
|
|
6037
|
-
orchestrator {
|
|
6697
|
+
orchestrator {
|
|
6698
|
+
model: opus
|
|
6699
|
+
handles: [intake]
|
|
6700
|
+
}
|
|
6038
6701
|
action intake { kind: inline }
|
|
6039
6702
|
agent a1 { model: sonnet }
|
|
6040
6703
|
group chat {
|
|
@@ -6052,7 +6715,10 @@ describe("Group chat node (F36)", () => {
|
|
|
6052
6715
|
});
|
|
6053
6716
|
it("V65: max-rounds is 0 — error", () => {
|
|
6054
6717
|
const src = `topology t : [pipeline] {
|
|
6055
|
-
orchestrator {
|
|
6718
|
+
orchestrator {
|
|
6719
|
+
model: opus
|
|
6720
|
+
handles: [intake]
|
|
6721
|
+
}
|
|
6056
6722
|
action intake { kind: inline }
|
|
6057
6723
|
agent a1 { model: sonnet }
|
|
6058
6724
|
group chat {
|
|
@@ -6069,7 +6735,10 @@ describe("Group chat node (F36)", () => {
|
|
|
6069
6735
|
});
|
|
6070
6736
|
it("group node with minimal fields (just members) — succeeds", () => {
|
|
6071
6737
|
const src = `topology t : [pipeline] {
|
|
6072
|
-
orchestrator {
|
|
6738
|
+
orchestrator {
|
|
6739
|
+
model: opus
|
|
6740
|
+
handles: [intake]
|
|
6741
|
+
}
|
|
6073
6742
|
action intake { kind: inline }
|
|
6074
6743
|
agent a1 { model: sonnet }
|
|
6075
6744
|
agent a2 { model: sonnet }
|
|
@@ -6096,7 +6765,10 @@ describe("Group chat node (F36)", () => {
|
|
|
6096
6765
|
describe("Rate limiting (F39)", () => {
|
|
6097
6766
|
it("parses rate-limit: 60/min", () => {
|
|
6098
6767
|
const src = `topology t : [pipeline] {
|
|
6099
|
-
orchestrator {
|
|
6768
|
+
orchestrator {
|
|
6769
|
+
model: opus
|
|
6770
|
+
handles: [intake]
|
|
6771
|
+
}
|
|
6100
6772
|
action intake { kind: inline }
|
|
6101
6773
|
agent api-caller {
|
|
6102
6774
|
model: sonnet
|
|
@@ -6111,7 +6783,10 @@ describe("Rate limiting (F39)", () => {
|
|
|
6111
6783
|
});
|
|
6112
6784
|
it("parses rate-limit: 1000/hour", () => {
|
|
6113
6785
|
const src = `topology t : [pipeline] {
|
|
6114
|
-
orchestrator {
|
|
6786
|
+
orchestrator {
|
|
6787
|
+
model: opus
|
|
6788
|
+
handles: [intake]
|
|
6789
|
+
}
|
|
6115
6790
|
action intake { kind: inline }
|
|
6116
6791
|
agent api-caller {
|
|
6117
6792
|
model: sonnet
|
|
@@ -6125,7 +6800,10 @@ describe("Rate limiting (F39)", () => {
|
|
|
6125
6800
|
});
|
|
6126
6801
|
it("V66: invalid format rate-limit: fast — error", () => {
|
|
6127
6802
|
const src = `topology t : [pipeline] {
|
|
6128
|
-
orchestrator {
|
|
6803
|
+
orchestrator {
|
|
6804
|
+
model: opus
|
|
6805
|
+
handles: [intake]
|
|
6806
|
+
}
|
|
6129
6807
|
action intake { kind: inline }
|
|
6130
6808
|
agent api-caller {
|
|
6131
6809
|
model: sonnet
|
|
@@ -6142,7 +6820,10 @@ describe("Rate limiting (F39)", () => {
|
|
|
6142
6820
|
});
|
|
6143
6821
|
it("V66: invalid unit rate-limit: 60/week — error", () => {
|
|
6144
6822
|
const src = `topology t : [pipeline] {
|
|
6145
|
-
orchestrator {
|
|
6823
|
+
orchestrator {
|
|
6824
|
+
model: opus
|
|
6825
|
+
handles: [intake]
|
|
6826
|
+
}
|
|
6146
6827
|
action intake { kind: inline }
|
|
6147
6828
|
agent api-caller {
|
|
6148
6829
|
model: sonnet
|
|
@@ -6158,7 +6839,10 @@ describe("Rate limiting (F39)", () => {
|
|
|
6158
6839
|
});
|
|
6159
6840
|
it("V66: zero rate rate-limit: 0/min — error", () => {
|
|
6160
6841
|
const src = `topology t : [pipeline] {
|
|
6161
|
-
orchestrator {
|
|
6842
|
+
orchestrator {
|
|
6843
|
+
model: opus
|
|
6844
|
+
handles: [intake]
|
|
6845
|
+
}
|
|
6162
6846
|
action intake { kind: inline }
|
|
6163
6847
|
agent api-caller {
|
|
6164
6848
|
model: sonnet
|
|
@@ -6175,7 +6859,10 @@ describe("Rate limiting (F39)", () => {
|
|
|
6175
6859
|
it("valid rate-limit values pass validation", () => {
|
|
6176
6860
|
for (const rl of ["1/sec", "60/min", "1000/hour", "10000/day"]) {
|
|
6177
6861
|
const src = `topology t : [pipeline] {
|
|
6178
|
-
orchestrator {
|
|
6862
|
+
orchestrator {
|
|
6863
|
+
model: opus
|
|
6864
|
+
handles: [intake]
|
|
6865
|
+
}
|
|
6179
6866
|
action intake { kind: inline }
|
|
6180
6867
|
agent api-caller {
|
|
6181
6868
|
model: sonnet
|
|
@@ -6324,7 +7011,10 @@ describe("Registry imports (F28-F30)", () => {
|
|
|
6324
7011
|
describe("Prompt variants (F40)", () => {
|
|
6325
7012
|
it("parses variants block in agent", () => {
|
|
6326
7013
|
const src = `topology t : [pipeline] {
|
|
6327
|
-
orchestrator {
|
|
7014
|
+
orchestrator {
|
|
7015
|
+
model: opus
|
|
7016
|
+
handles: [intake]
|
|
7017
|
+
}
|
|
6328
7018
|
action intake { kind: inline }
|
|
6329
7019
|
agent reviewer {
|
|
6330
7020
|
model: sonnet
|
|
@@ -6357,7 +7047,10 @@ describe("Prompt variants (F40)", () => {
|
|
|
6357
7047
|
});
|
|
6358
7048
|
it("parses variant with model override", () => {
|
|
6359
7049
|
const src = `topology t : [pipeline] {
|
|
6360
|
-
orchestrator {
|
|
7050
|
+
orchestrator {
|
|
7051
|
+
model: opus
|
|
7052
|
+
handles: [intake]
|
|
7053
|
+
}
|
|
6361
7054
|
action intake { kind: inline }
|
|
6362
7055
|
agent reviewer {
|
|
6363
7056
|
model: sonnet
|
|
@@ -6449,7 +7142,10 @@ describe("Prompt variants (F40)", () => {
|
|
|
6449
7142
|
describe("Encrypted values (F41)", () => {
|
|
6450
7143
|
it("parses encrypted value in env block", () => {
|
|
6451
7144
|
const src = `topology t : [pipeline] {
|
|
6452
|
-
orchestrator {
|
|
7145
|
+
orchestrator {
|
|
7146
|
+
model: opus
|
|
7147
|
+
handles: [intake]
|
|
7148
|
+
}
|
|
6453
7149
|
action intake { kind: inline }
|
|
6454
7150
|
agent w { model: sonnet }
|
|
6455
7151
|
env {
|
|
@@ -6466,7 +7162,10 @@ describe("Encrypted values (F41)", () => {
|
|
|
6466
7162
|
});
|
|
6467
7163
|
it("encrypted implies sensitive", () => {
|
|
6468
7164
|
const src = `topology t : [pipeline] {
|
|
6469
|
-
orchestrator {
|
|
7165
|
+
orchestrator {
|
|
7166
|
+
model: opus
|
|
7167
|
+
handles: [intake]
|
|
7168
|
+
}
|
|
6470
7169
|
action intake { kind: inline }
|
|
6471
7170
|
agent w { model: sonnet }
|
|
6472
7171
|
env {
|
|
@@ -6518,7 +7217,10 @@ describe("Encrypted values (F41)", () => {
|
|
|
6518
7217
|
describe("Auth block in providers (F42)", () => {
|
|
6519
7218
|
it("parses auth block in provider", () => {
|
|
6520
7219
|
const src = `topology t : [pipeline] {
|
|
6521
|
-
orchestrator {
|
|
7220
|
+
orchestrator {
|
|
7221
|
+
model: opus
|
|
7222
|
+
handles: [intake]
|
|
7223
|
+
}
|
|
6522
7224
|
action intake { kind: inline }
|
|
6523
7225
|
agent w { model: sonnet }
|
|
6524
7226
|
providers {
|
|
@@ -6543,7 +7245,10 @@ describe("Auth block in providers (F42)", () => {
|
|
|
6543
7245
|
});
|
|
6544
7246
|
it("provider without auth block (backward compat) passes", () => {
|
|
6545
7247
|
const src = `topology t : [pipeline] {
|
|
6546
|
-
orchestrator {
|
|
7248
|
+
orchestrator {
|
|
7249
|
+
model: opus
|
|
7250
|
+
handles: [intake]
|
|
7251
|
+
}
|
|
6547
7252
|
action intake { kind: inline }
|
|
6548
7253
|
agent w { model: sonnet }
|
|
6549
7254
|
providers {
|
|
@@ -6618,7 +7323,14 @@ describe("Auth block in providers (F42)", () => {
|
|
|
6618
7323
|
// =========================================================================
|
|
6619
7324
|
describe("store and retrieval blocks", () => {
|
|
6620
7325
|
it("parses a basic store with type, backend, and path", () => {
|
|
6621
|
-
const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
|
|
7326
|
+
const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
|
|
7327
|
+
model: opus
|
|
7328
|
+
handles: [a]
|
|
7329
|
+
}\n action a { kind: inline }\n flow { a -> a }\n memory {\n store my-store {
|
|
7330
|
+
type: semantic\n
|
|
7331
|
+
backend: lancedb\n
|
|
7332
|
+
path: ".memory/"\n
|
|
7333
|
+
}\n }\n}`;
|
|
6622
7334
|
const ast = parse(src);
|
|
6623
7335
|
expect(ast.stores).toHaveLength(1);
|
|
6624
7336
|
expect(ast.stores[0].id).toBe("my-store");
|
|
@@ -6627,7 +7339,19 @@ describe("Auth block in providers (F42)", () => {
|
|
|
6627
7339
|
expect(ast.stores[0].path).toBe(".memory/");
|
|
6628
7340
|
});
|
|
6629
7341
|
it("parses multiple stores in one memory block", () => {
|
|
6630
|
-
const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
|
|
7342
|
+
const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
|
|
7343
|
+
model: opus
|
|
7344
|
+
handles: [a]
|
|
7345
|
+
}\n action a { kind: inline }\n flow { a -> a }\n memory {\n store kb {
|
|
7346
|
+
type: semantic\n
|
|
7347
|
+
backend: lancedb\n
|
|
7348
|
+
}\n store relations {
|
|
7349
|
+
type: graph\n
|
|
7350
|
+
backend: kuzu\n
|
|
7351
|
+
}\n store history {
|
|
7352
|
+
type: episodic\n
|
|
7353
|
+
backend: sqlite-vec\n
|
|
7354
|
+
}\n }\n}`;
|
|
6631
7355
|
const ast = parse(src);
|
|
6632
7356
|
expect(ast.stores).toHaveLength(3);
|
|
6633
7357
|
expect(ast.stores[0].id).toBe("kb");
|
|
@@ -6727,7 +7451,14 @@ describe("Auth block in providers (F42)", () => {
|
|
|
6727
7451
|
expect(store.backendConfig["replicas"]).toBe(3);
|
|
6728
7452
|
});
|
|
6729
7453
|
it("parses store with connection secret", () => {
|
|
6730
|
-
const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
|
|
7454
|
+
const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
|
|
7455
|
+
model: opus
|
|
7456
|
+
handles: [a]
|
|
7457
|
+
}\n action a { kind: inline }\n flow { a -> a }\n memory {\n store remote-db {
|
|
7458
|
+
type: semantic\n
|
|
7459
|
+
backend: pinecone\n
|
|
7460
|
+
connection: secret "DB_URL"\n
|
|
7461
|
+
}\n }\n}`;
|
|
6731
7462
|
const ast = parse(src);
|
|
6732
7463
|
expect(ast.stores).toHaveLength(1);
|
|
6733
7464
|
expect(ast.stores[0].connection).toBe("DB_URL");
|
|
@@ -6818,7 +7549,10 @@ describe("Auth block in providers (F42)", () => {
|
|
|
6818
7549
|
expect(agent.retrieval).toBe("planner");
|
|
6819
7550
|
});
|
|
6820
7551
|
it("parses empty memory block with no stores", () => {
|
|
6821
|
-
const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
|
|
7552
|
+
const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
|
|
7553
|
+
model: opus
|
|
7554
|
+
handles: [a]
|
|
7555
|
+
}\n action a { kind: inline }\n flow { a -> a }\n memory {}\n}`;
|
|
6822
7556
|
const ast = parse(src);
|
|
6823
7557
|
expect(ast.stores).toEqual([]);
|
|
6824
7558
|
expect(ast.retrievals).toEqual([]);
|
|
@@ -7059,4 +7793,616 @@ describe("Auth block in providers (F42)", () => {
|
|
|
7059
7793
|
});
|
|
7060
7794
|
});
|
|
7061
7795
|
});
|
|
7796
|
+
describe("Company brain — pattern, brain store type, custody primitive", () => {
|
|
7797
|
+
const brainSrc = `topology cb : [brain] {
|
|
7798
|
+
meta {
|
|
7799
|
+
version: "1.0.0"
|
|
7800
|
+
description: "x"
|
|
7801
|
+
}
|
|
7802
|
+
memory {
|
|
7803
|
+
store brain {
|
|
7804
|
+
type: brain
|
|
7805
|
+
description: "the company brain"
|
|
7806
|
+
path: "brain/"
|
|
7807
|
+
format: obsidian
|
|
7808
|
+
}
|
|
7809
|
+
}
|
|
7810
|
+
agent librarian {
|
|
7811
|
+
model: sonnet
|
|
7812
|
+
description: "keeps the brain"
|
|
7813
|
+
tools: [Read, Write, Grep]
|
|
7814
|
+
custodian-of: [brain] {
|
|
7815
|
+
does: [link, tag, index, dedupe]
|
|
7816
|
+
}
|
|
7817
|
+
}
|
|
7818
|
+
agent researcher {
|
|
7819
|
+
model: sonnet
|
|
7820
|
+
description: "reads the brain"
|
|
7821
|
+
tools: [Read, Grep]
|
|
7822
|
+
memory: [brain]
|
|
7823
|
+
}
|
|
7824
|
+
action intake {
|
|
7825
|
+
kind: inline
|
|
7826
|
+
description: "in"
|
|
7827
|
+
}
|
|
7828
|
+
flow {
|
|
7829
|
+
intake -> librarian
|
|
7830
|
+
librarian -> researcher
|
|
7831
|
+
}
|
|
7832
|
+
}`;
|
|
7833
|
+
it("parses a brain-type store with files backend default and format", () => {
|
|
7834
|
+
const ast = parse(brainSrc);
|
|
7835
|
+
const store = (ast.stores ?? []).find((s) => s.id === "brain");
|
|
7836
|
+
expect(store).toBeDefined();
|
|
7837
|
+
expect(store.type).toBe("brain");
|
|
7838
|
+
expect(store.backend).toBe("files"); // file-native default, not lancedb
|
|
7839
|
+
expect(store.format).toBe("obsidian");
|
|
7840
|
+
expect(store.path).toBe("brain/");
|
|
7841
|
+
});
|
|
7842
|
+
it("parses custodian-of and the optional does verbs on an agent", () => {
|
|
7843
|
+
const ast = parse(brainSrc);
|
|
7844
|
+
const librarian = ast.nodes.find((n) => n.id === "librarian");
|
|
7845
|
+
expect(librarian.custodianOf).toEqual(["brain"]);
|
|
7846
|
+
expect(librarian.custodianDoes).toEqual(["link", "tag", "index", "dedupe"]);
|
|
7847
|
+
});
|
|
7848
|
+
it("custody is optional — bare custodian-of with no verbs parses", () => {
|
|
7849
|
+
const src = brainSrc.replace("custodian-of: [brain] {\n does: [link, tag, index, dedupe]\n }", "custodian-of: [brain]");
|
|
7850
|
+
const ast = parse(src);
|
|
7851
|
+
const librarian = ast.nodes.find((n) => n.id === "librarian");
|
|
7852
|
+
expect(librarian.custodianOf).toEqual(["brain"]);
|
|
7853
|
+
expect(librarian.custodianDoes).toBeUndefined();
|
|
7854
|
+
});
|
|
7855
|
+
it("the brain pattern tag is recognized (no V2 reserved-keyword error)", () => {
|
|
7856
|
+
const ast = parse(brainSrc);
|
|
7857
|
+
expect(ast.topology.patterns).toContain("brain");
|
|
7858
|
+
const results = validate(ast);
|
|
7859
|
+
expect(results.filter((r) => r.level === "error")).toHaveLength(0);
|
|
7860
|
+
});
|
|
7861
|
+
it("V81: a brain store needs no DB backend (exempt from backend enum)", () => {
|
|
7862
|
+
const ast = parse(brainSrc);
|
|
7863
|
+
const results = validate(ast);
|
|
7864
|
+
expect(results.filter((r) => r.rule === "V81")).toHaveLength(0);
|
|
7865
|
+
});
|
|
7866
|
+
it("V88: custodian-of referencing an undeclared store is an error", () => {
|
|
7867
|
+
const ast = minimalASTForWave6({
|
|
7868
|
+
nodes: [
|
|
7869
|
+
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
7870
|
+
{ id: "intake", type: "action", label: "Intake" },
|
|
7871
|
+
{ id: "keeper", type: "agent", label: "Keeper", model: "sonnet", custodianOf: ["ghost-store"] },
|
|
7872
|
+
],
|
|
7873
|
+
edges: [{ from: "intake", to: "keeper", condition: null, maxIterations: null }],
|
|
7874
|
+
stores: [{ id: "brain", type: "brain", backend: "files" }],
|
|
7875
|
+
retrievals: [],
|
|
7876
|
+
});
|
|
7877
|
+
const v88 = validate(ast).filter((r) => r.rule === "V88" && r.level === "error");
|
|
7878
|
+
expect(v88).toHaveLength(1);
|
|
7879
|
+
expect(v88[0].message).toContain("ghost-store");
|
|
7880
|
+
});
|
|
7881
|
+
it("V88: custody over a declared brain store is clean", () => {
|
|
7882
|
+
const ast = minimalASTForWave6({
|
|
7883
|
+
nodes: [
|
|
7884
|
+
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
7885
|
+
{ id: "intake", type: "action", label: "Intake" },
|
|
7886
|
+
{ id: "keeper", type: "agent", label: "Keeper", model: "sonnet", custodianOf: ["brain"] },
|
|
7887
|
+
],
|
|
7888
|
+
edges: [{ from: "intake", to: "keeper", condition: null, maxIterations: null }],
|
|
7889
|
+
stores: [{ id: "brain", type: "brain", backend: "files" }],
|
|
7890
|
+
retrievals: [],
|
|
7891
|
+
});
|
|
7892
|
+
const v88 = validate(ast).filter((r) => r.rule === "V88");
|
|
7893
|
+
expect(v88).toHaveLength(0);
|
|
7894
|
+
});
|
|
7895
|
+
it("V88: custody over a non-brain store is a warning, not an error", () => {
|
|
7896
|
+
const ast = minimalASTForWave6({
|
|
7897
|
+
nodes: [
|
|
7898
|
+
{ id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
|
|
7899
|
+
{ id: "intake", type: "action", label: "Intake" },
|
|
7900
|
+
{ id: "keeper", type: "agent", label: "Keeper", model: "sonnet", custodianOf: ["kg"] },
|
|
7901
|
+
],
|
|
7902
|
+
edges: [{ from: "intake", to: "keeper", condition: null, maxIterations: null }],
|
|
7903
|
+
stores: [{ id: "kg", type: "graph", backend: "kuzu" }],
|
|
7904
|
+
retrievals: [],
|
|
7905
|
+
});
|
|
7906
|
+
const v88 = validate(ast).filter((r) => r.rule === "V88");
|
|
7907
|
+
expect(v88).toHaveLength(1);
|
|
7908
|
+
expect(v88[0].level).toBe("warning");
|
|
7909
|
+
});
|
|
7910
|
+
it("parses a brain store's presentation-only `sources` block", () => {
|
|
7911
|
+
const src = `topology cb : [brain] {
|
|
7912
|
+
meta {
|
|
7913
|
+
version: "1.0.0"
|
|
7914
|
+
description: "x"
|
|
7915
|
+
}
|
|
7916
|
+
memory {
|
|
7917
|
+
store brain {
|
|
7918
|
+
type: brain
|
|
7919
|
+
path: "brain/"
|
|
7920
|
+
sources {
|
|
7921
|
+
gmail {
|
|
7922
|
+
color: "#EA4335"
|
|
7923
|
+
icon: "./logos/gmail.svg"
|
|
7924
|
+
}
|
|
7925
|
+
slack {
|
|
7926
|
+
color: "#4A154B"
|
|
7927
|
+
}
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
}
|
|
7931
|
+
agent lib {
|
|
7932
|
+
model: sonnet
|
|
7933
|
+
description: "k"
|
|
7934
|
+
tools: [Read, Write]
|
|
7935
|
+
custodian-of: [brain]
|
|
7936
|
+
}
|
|
7937
|
+
action intake {
|
|
7938
|
+
kind: inline
|
|
7939
|
+
description: "in"
|
|
7940
|
+
}
|
|
7941
|
+
flow { intake -> lib }
|
|
7942
|
+
}`;
|
|
7943
|
+
const ast = parse(src);
|
|
7944
|
+
const store = (ast.stores ?? []).find((s) => s.id === "brain");
|
|
7945
|
+
expect(store?.sources).toEqual({
|
|
7946
|
+
gmail: { color: "#EA4335", icon: "./logos/gmail.svg" },
|
|
7947
|
+
slack: { color: "#4A154B" },
|
|
7948
|
+
});
|
|
7949
|
+
// It is presentation-only — purely data on the AST, no validation error.
|
|
7950
|
+
expect(validate(ast).filter((r) => r.level === "error")).toHaveLength(0);
|
|
7951
|
+
});
|
|
7952
|
+
it("V24: a store's `sources` sub-block does not trigger memory sub-block warnings", () => {
|
|
7953
|
+
// Regression: the V24 scanner used to regex the whole memory body flatly,
|
|
7954
|
+
// flagging blocks NESTED in a store (sources/gmail/slack) as unknown memory
|
|
7955
|
+
// sub-blocks. The scanner is now depth-aware — only top-level memory blocks
|
|
7956
|
+
// are checked, and `store <id> {` is recognized by its leading keyword.
|
|
7957
|
+
const src = `topology cb : [brain] {
|
|
7958
|
+
meta {
|
|
7959
|
+
version: "1.0.0"
|
|
7960
|
+
description: "x"
|
|
7961
|
+
}
|
|
7962
|
+
memory {
|
|
7963
|
+
store brain {
|
|
7964
|
+
type: brain
|
|
7965
|
+
path: "brain/"
|
|
7966
|
+
scope: org
|
|
7967
|
+
sources {
|
|
7968
|
+
gmail { color: "#EA4335" }
|
|
7969
|
+
slack { color: "#4A154B" }
|
|
7970
|
+
}
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
agent lib {
|
|
7974
|
+
model: sonnet
|
|
7975
|
+
description: "k"
|
|
7976
|
+
tools: [Read, Write]
|
|
7977
|
+
custodian-of: [brain]
|
|
7978
|
+
}
|
|
7979
|
+
action intake {
|
|
7980
|
+
kind: inline
|
|
7981
|
+
description: "in"
|
|
7982
|
+
}
|
|
7983
|
+
flow { intake -> lib }
|
|
7984
|
+
}`;
|
|
7985
|
+
const v24 = validate(parse(src)).filter((r) => r.rule === "V24");
|
|
7986
|
+
expect(v24).toHaveLength(0);
|
|
7987
|
+
});
|
|
7988
|
+
it("V89: a KV `prompt:` on an agent is an error, not a silent drop", () => {
|
|
7989
|
+
// Regression: spec/grammar.md:350 defines agent.prompt as a BLOCK. The
|
|
7990
|
+
// string form belongs to `skill` (grammar.md:1377). Writing
|
|
7991
|
+
// `prompt: "path.md"` on an agent parsed to nothing and validate() said
|
|
7992
|
+
// PASS — so the agent shipped with no instructions and the binding fell
|
|
7993
|
+
// back to repeating its role sentence. examples/code-review.at did this on
|
|
7994
|
+
// four agents, pointing at a prompts/ directory that never existed.
|
|
7995
|
+
const src = `topology t : [pipeline] {
|
|
7996
|
+
meta {
|
|
7997
|
+
version: "1.0.0"
|
|
7998
|
+
description: "x"
|
|
7999
|
+
}
|
|
8000
|
+
agent worker {
|
|
8001
|
+
model: sonnet
|
|
8002
|
+
description: "w"
|
|
8003
|
+
prompt: "prompts/worker.md"
|
|
8004
|
+
}
|
|
8005
|
+
action intake {
|
|
8006
|
+
kind: inline
|
|
8007
|
+
description: "in"
|
|
8008
|
+
}
|
|
8009
|
+
flow { intake -> worker }
|
|
8010
|
+
}`;
|
|
8011
|
+
const ast = parse(src);
|
|
8012
|
+
expect(ast.nodes.find((n) => n.id === "worker")).not.toHaveProperty("prompt");
|
|
8013
|
+
const v89 = validate(ast).filter((r) => r.rule === "V89");
|
|
8014
|
+
expect(v89).toHaveLength(1);
|
|
8015
|
+
expect(v89[0].level).toBe("error");
|
|
8016
|
+
expect(v89[0].node).toBe("worker");
|
|
8017
|
+
});
|
|
8018
|
+
it("V89: the `prompt { }` block form is accepted and reaches the AST", () => {
|
|
8019
|
+
const src = `topology t : [pipeline] {
|
|
8020
|
+
meta {
|
|
8021
|
+
version: "1.0.0"
|
|
8022
|
+
description: "x"
|
|
8023
|
+
}
|
|
8024
|
+
agent worker {
|
|
8025
|
+
model: sonnet
|
|
8026
|
+
description: "w"
|
|
8027
|
+
prompt {
|
|
8028
|
+
Do the thing.
|
|
8029
|
+
}
|
|
8030
|
+
}
|
|
8031
|
+
action intake {
|
|
8032
|
+
kind: inline
|
|
8033
|
+
description: "in"
|
|
8034
|
+
}
|
|
8035
|
+
flow { intake -> worker }
|
|
8036
|
+
}`;
|
|
8037
|
+
const ast = parse(src);
|
|
8038
|
+
const worker = ast.nodes.find((n) => n.id === "worker");
|
|
8039
|
+
expect(worker.prompt).toBe("Do the thing.");
|
|
8040
|
+
expect(validate(ast).filter((r) => r.rule === "V89")).toHaveLength(0);
|
|
8041
|
+
});
|
|
8042
|
+
it("V90: a collapsed meta block is caught — it corrupts the version", () => {
|
|
8043
|
+
// The damaging case. `version` silently became `1.0.0" description: "x`,
|
|
8044
|
+
// which then printed in every binding's output and in the plan brief.
|
|
8045
|
+
const src = [
|
|
8046
|
+
"topology t : [pipeline] {",
|
|
8047
|
+
' meta { version: "1.0.0" description: "x" }',
|
|
8048
|
+
" agent a {",
|
|
8049
|
+
" model: sonnet",
|
|
8050
|
+
' description: "a"',
|
|
8051
|
+
" }",
|
|
8052
|
+
" action i {",
|
|
8053
|
+
" kind: inline",
|
|
8054
|
+
' description: "in"',
|
|
8055
|
+
" }",
|
|
8056
|
+
" flow { i -> a }",
|
|
8057
|
+
"}",
|
|
8058
|
+
].join("\n");
|
|
8059
|
+
const v90 = validate(parse(src)).filter((r) => r.rule === "V90");
|
|
8060
|
+
expect(v90).toHaveLength(1);
|
|
8061
|
+
expect(v90[0].message).toContain("version");
|
|
8062
|
+
});
|
|
8063
|
+
it("V90: a collapsed agent block is caught", () => {
|
|
8064
|
+
const src = [
|
|
8065
|
+
"topology t : [pipeline] {",
|
|
8066
|
+
" meta {",
|
|
8067
|
+
' version: "1.0.0"',
|
|
8068
|
+
' description: "x"',
|
|
8069
|
+
" }",
|
|
8070
|
+
" agent a { model: sonnet retry: 3 }",
|
|
8071
|
+
" action i {",
|
|
8072
|
+
" kind: inline",
|
|
8073
|
+
' description: "in"',
|
|
8074
|
+
" }",
|
|
8075
|
+
" flow { i -> a }",
|
|
8076
|
+
"}",
|
|
8077
|
+
].join("\n");
|
|
8078
|
+
const v90 = validate(parse(src)).filter((r) => r.rule === "V90");
|
|
8079
|
+
expect(v90.some((r) => r.message.includes("model"))).toBe(true);
|
|
8080
|
+
});
|
|
8081
|
+
it("V90: a colon inside a prose description is NOT flagged", () => {
|
|
8082
|
+
// `parseMeta` strips quotes, so a description arrives as bare prose. A
|
|
8083
|
+
// colon in English is ordinary — "Demo: agent pipeline with DuckDB".
|
|
8084
|
+
// Two shipped examples do exactly this and must stay clean.
|
|
8085
|
+
const src = [
|
|
8086
|
+
"topology t : [pipeline] {",
|
|
8087
|
+
" meta {",
|
|
8088
|
+
' version: "1.0.0"',
|
|
8089
|
+
' description: "Demo: agent pipeline with DuckDB storage"',
|
|
8090
|
+
" }",
|
|
8091
|
+
" agent a {",
|
|
8092
|
+
" model: sonnet",
|
|
8093
|
+
' description: "Static analysis: complexity and blast radius"',
|
|
8094
|
+
" }",
|
|
8095
|
+
" action i {",
|
|
8096
|
+
" kind: inline",
|
|
8097
|
+
' description: "in"',
|
|
8098
|
+
" }",
|
|
8099
|
+
" flow { i -> a }",
|
|
8100
|
+
"}",
|
|
8101
|
+
].join("\n");
|
|
8102
|
+
expect(validate(parse(src)).filter((r) => r.rule === "V90")).toHaveLength(0);
|
|
8103
|
+
});
|
|
8104
|
+
it("V90: a quoted value FOLLOWED by more content is still a swallow", () => {
|
|
8105
|
+
// Where quotes survive parsing, the check is sharper: one well-formed
|
|
8106
|
+
// quoted token then more content means a field was eaten.
|
|
8107
|
+
const src = [
|
|
8108
|
+
"topology t : [pipeline] {",
|
|
8109
|
+
" meta {",
|
|
8110
|
+
' version: "1.0.0"',
|
|
8111
|
+
' description: "x"',
|
|
8112
|
+
" }",
|
|
8113
|
+
" agent a {",
|
|
8114
|
+
' description: "k" model: sonnet',
|
|
8115
|
+
" }",
|
|
8116
|
+
" action i {",
|
|
8117
|
+
" kind: inline",
|
|
8118
|
+
' description: "in"',
|
|
8119
|
+
" }",
|
|
8120
|
+
" flow { i -> a }",
|
|
8121
|
+
"}",
|
|
8122
|
+
].join("\n");
|
|
8123
|
+
expect(validate(parse(src)).filter((r) => r.rule === "V90").length).toBeGreaterThan(0);
|
|
8124
|
+
});
|
|
8125
|
+
it("V90: every shipped example is free of swallowed fields", () => {
|
|
8126
|
+
const dir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../examples");
|
|
8127
|
+
for (const f of readdirSync(dir).filter((x) => x.endsWith(".at"))) {
|
|
8128
|
+
const v90 = validate(parse(readFileSync(resolve(dir, f), "utf8"))).filter((r) => r.rule === "V90");
|
|
8129
|
+
expect(v90, `${f}: ${v90.map((r) => r.message).join("; ")}`).toHaveLength(0);
|
|
8130
|
+
}
|
|
8131
|
+
});
|
|
8132
|
+
it("V90: a spec-legal single-line nested object is NOT a swallow", () => {
|
|
8133
|
+
// `spec/grammar.md:117` gives `outputs: { depth: 1 | 2 | 3 }` verbatim as
|
|
8134
|
+
// legal, and it parses correctly. V90 flagged it, which rejected valid
|
|
8135
|
+
// topologies — a false positive is worse than a miss.
|
|
8136
|
+
const src = [
|
|
8137
|
+
"topology t : [pipeline] {",
|
|
8138
|
+
" meta {",
|
|
8139
|
+
' version: "1.0.0"',
|
|
8140
|
+
' description: "x"',
|
|
8141
|
+
" }",
|
|
8142
|
+
" agent a {",
|
|
8143
|
+
" model: sonnet",
|
|
8144
|
+
' description: "a"',
|
|
8145
|
+
" outputs: { verdict: approve | reject }",
|
|
8146
|
+
" tools: [Read, Write]",
|
|
8147
|
+
" }",
|
|
8148
|
+
" action i {",
|
|
8149
|
+
" kind: inline",
|
|
8150
|
+
' description: "in"',
|
|
8151
|
+
" }",
|
|
8152
|
+
" flow { i -> a }",
|
|
8153
|
+
"}",
|
|
8154
|
+
].join("\n");
|
|
8155
|
+
const ast = parse(src);
|
|
8156
|
+
expect(validate(ast).filter((r) => r.rule === "V90")).toHaveLength(0);
|
|
8157
|
+
// and it really did parse — the exemption must not hide a broken parse
|
|
8158
|
+
const a = ast.nodes.find((n) => n.id === "a");
|
|
8159
|
+
expect(a.outputs?.verdict).toEqual(["approve", "reject"]);
|
|
8160
|
+
});
|
|
8161
|
+
it("V90: a swallow with no space after the colon is still caught", () => {
|
|
8162
|
+
const src = [
|
|
8163
|
+
"topology t : [pipeline] {",
|
|
8164
|
+
" meta {",
|
|
8165
|
+
' version: "1.0.0"',
|
|
8166
|
+
' description: "x"',
|
|
8167
|
+
" }",
|
|
8168
|
+
" agent a { model: sonnet retry:3 }",
|
|
8169
|
+
" action i {",
|
|
8170
|
+
" kind: inline",
|
|
8171
|
+
' description: "in"',
|
|
8172
|
+
" }",
|
|
8173
|
+
" flow { i -> a }",
|
|
8174
|
+
"}",
|
|
8175
|
+
].join("\n");
|
|
8176
|
+
expect(validate(parse(src)).filter((r) => r.rule === "V90").length).toBeGreaterThan(0);
|
|
8177
|
+
});
|
|
8178
|
+
it("V90: orchestrator, human and group blocks are checked too", () => {
|
|
8179
|
+
const src = [
|
|
8180
|
+
"topology t : [pipeline] {",
|
|
8181
|
+
" meta {",
|
|
8182
|
+
' version: "1.0.0"',
|
|
8183
|
+
' description: "x"',
|
|
8184
|
+
" }",
|
|
8185
|
+
" orchestrator { model: opus handles: [i] }",
|
|
8186
|
+
" agent a {",
|
|
8187
|
+
" model: sonnet",
|
|
8188
|
+
' description: "a"',
|
|
8189
|
+
" }",
|
|
8190
|
+
" action i {",
|
|
8191
|
+
" kind: inline",
|
|
8192
|
+
' description: "in"',
|
|
8193
|
+
" }",
|
|
8194
|
+
" flow { i -> a }",
|
|
8195
|
+
"}",
|
|
8196
|
+
].join("\n");
|
|
8197
|
+
const v90 = validate(parse(src)).filter((r) => r.rule === "V90");
|
|
8198
|
+
expect(v90.length).toBeGreaterThan(0);
|
|
8199
|
+
expect(v90[0].node).toBe("orchestrator");
|
|
8200
|
+
});
|
|
8201
|
+
it("V92: two bracket groups on an edge are rejected, not silently mangled", () => {
|
|
8202
|
+
// The annotation matcher spanned from the first `[` to the last `]`, so
|
|
8203
|
+
// `[when a.verdict == fail] [max 3]` produced the condition string
|
|
8204
|
+
// `a.verdict == fail] [max 3` — brackets and all — with no error.
|
|
8205
|
+
const src = [
|
|
8206
|
+
"topology t : [pipeline] {",
|
|
8207
|
+
" meta {",
|
|
8208
|
+
' version: "1.0.0"',
|
|
8209
|
+
' description: "x"',
|
|
8210
|
+
" }",
|
|
8211
|
+
" agent a {",
|
|
8212
|
+
" model: sonnet",
|
|
8213
|
+
' description: "a"',
|
|
8214
|
+
" outputs: {",
|
|
8215
|
+
" verdict: pass | fail",
|
|
8216
|
+
" }",
|
|
8217
|
+
" }",
|
|
8218
|
+
" agent b {",
|
|
8219
|
+
" model: sonnet",
|
|
8220
|
+
' description: "b"',
|
|
8221
|
+
" }",
|
|
8222
|
+
" action i {",
|
|
8223
|
+
" kind: inline",
|
|
8224
|
+
' description: "in"',
|
|
8225
|
+
" }",
|
|
8226
|
+
" flow { i -> a",
|
|
8227
|
+
" a -> b [when a.verdict == fail] [max 3] }",
|
|
8228
|
+
"}",
|
|
8229
|
+
].join("\n");
|
|
8230
|
+
const v92 = validate(parse(src)).filter((r) => r.rule === "V92");
|
|
8231
|
+
expect(v92).toHaveLength(1);
|
|
8232
|
+
expect(v92[0].message).toContain("ONE bracket");
|
|
8233
|
+
});
|
|
8234
|
+
it("V92: a fan-out list plus an annotation is legal, not two annotations", () => {
|
|
8235
|
+
// `analyzer -> [reporter, alerter] [when ...]` ships in
|
|
8236
|
+
// examples/scheduled-monitor.at. Testing for `] [` alone flagged it.
|
|
8237
|
+
const src = [
|
|
8238
|
+
"topology t : [pipeline, fan-out] {",
|
|
8239
|
+
" meta {",
|
|
8240
|
+
' version: "1.0.0"',
|
|
8241
|
+
' description: "x"',
|
|
8242
|
+
" }",
|
|
8243
|
+
" agent a {",
|
|
8244
|
+
" model: sonnet",
|
|
8245
|
+
' description: "a"',
|
|
8246
|
+
" outputs: {",
|
|
8247
|
+
" verdict: pass | fail",
|
|
8248
|
+
" }",
|
|
8249
|
+
" }",
|
|
8250
|
+
" agent b {",
|
|
8251
|
+
" model: sonnet",
|
|
8252
|
+
' description: "b"',
|
|
8253
|
+
" }",
|
|
8254
|
+
" agent c {",
|
|
8255
|
+
" model: sonnet",
|
|
8256
|
+
' description: "c"',
|
|
8257
|
+
" }",
|
|
8258
|
+
" action i {",
|
|
8259
|
+
" kind: inline",
|
|
8260
|
+
' description: "in"',
|
|
8261
|
+
" }",
|
|
8262
|
+
" flow { i -> a",
|
|
8263
|
+
" a -> [b, c] [when a.verdict == fail] }",
|
|
8264
|
+
"}",
|
|
8265
|
+
].join("\n");
|
|
8266
|
+
expect(validate(parse(src)).filter((r) => r.rule === "V92")).toHaveLength(0);
|
|
8267
|
+
});
|
|
8268
|
+
it("V92: the legal single-bracket form is unaffected", () => {
|
|
8269
|
+
const src = [
|
|
8270
|
+
"topology t : [pipeline] {",
|
|
8271
|
+
" meta {",
|
|
8272
|
+
' version: "1.0.0"',
|
|
8273
|
+
' description: "x"',
|
|
8274
|
+
" }",
|
|
8275
|
+
" agent a {",
|
|
8276
|
+
" model: sonnet",
|
|
8277
|
+
' description: "a"',
|
|
8278
|
+
" outputs: {",
|
|
8279
|
+
" verdict: pass | fail",
|
|
8280
|
+
" }",
|
|
8281
|
+
" }",
|
|
8282
|
+
" agent b {",
|
|
8283
|
+
" model: sonnet",
|
|
8284
|
+
' description: "b"',
|
|
8285
|
+
" }",
|
|
8286
|
+
" action i {",
|
|
8287
|
+
" kind: inline",
|
|
8288
|
+
' description: "in"',
|
|
8289
|
+
" }",
|
|
8290
|
+
" flow { i -> a",
|
|
8291
|
+
" a -> b [when a.verdict == fail, max 3] }",
|
|
8292
|
+
"}",
|
|
8293
|
+
].join("\n");
|
|
8294
|
+
const ast = parse(src);
|
|
8295
|
+
expect(validate(ast).filter((r) => r.rule === "V92")).toHaveLength(0);
|
|
8296
|
+
const e = ast.edges.find((x) => x.from === "a" && x.to === "b");
|
|
8297
|
+
expect(e.condition).toBe("a.verdict == fail");
|
|
8298
|
+
expect(e.maxIterations).toBe(3);
|
|
8299
|
+
});
|
|
8300
|
+
it("V91: a required field satisfied by an injected placeholder is still missing", () => {
|
|
8301
|
+
// The parser substitutes a sentinel because the AST types are non-optional,
|
|
8302
|
+
// and every downstream "is it present?" check then passed. V7 (model
|
|
8303
|
+
// required) saw "unknown" and was satisfied; codex emitted it verbatim into
|
|
8304
|
+
// config.toml as a model that does not exist.
|
|
8305
|
+
const noVersion = [
|
|
8306
|
+
"topology t : [pipeline] {",
|
|
8307
|
+
" meta {",
|
|
8308
|
+
' description: "no version declared"',
|
|
8309
|
+
" }",
|
|
8310
|
+
" agent a {",
|
|
8311
|
+
" model: sonnet",
|
|
8312
|
+
' description: "a"',
|
|
8313
|
+
" }",
|
|
8314
|
+
" action i {",
|
|
8315
|
+
" kind: inline",
|
|
8316
|
+
' description: "in"',
|
|
8317
|
+
" }",
|
|
8318
|
+
" flow { i -> a }",
|
|
8319
|
+
"}",
|
|
8320
|
+
].join("\n");
|
|
8321
|
+
const ast = parse(noVersion);
|
|
8322
|
+
expect(ast.topology.version).toBe("0.0.0"); // the sentinel is still injected
|
|
8323
|
+
const v91 = validate(ast).filter((r) => r.rule === "V91");
|
|
8324
|
+
expect(v91).toHaveLength(1);
|
|
8325
|
+
expect(v91[0].message).toContain("version");
|
|
8326
|
+
});
|
|
8327
|
+
it("V91: an orchestrator with no model is caught, where V7 was satisfied", () => {
|
|
8328
|
+
const src = [
|
|
8329
|
+
"topology t : [pipeline] {",
|
|
8330
|
+
" meta {",
|
|
8331
|
+
' version: "1.0.0"',
|
|
8332
|
+
' description: "x"',
|
|
8333
|
+
" }",
|
|
8334
|
+
" orchestrator {",
|
|
8335
|
+
" handles: [i]",
|
|
8336
|
+
" }",
|
|
8337
|
+
" agent a {",
|
|
8338
|
+
" model: sonnet",
|
|
8339
|
+
' description: "a"',
|
|
8340
|
+
" }",
|
|
8341
|
+
" action i {",
|
|
8342
|
+
" kind: inline",
|
|
8343
|
+
' description: "in"',
|
|
8344
|
+
" }",
|
|
8345
|
+
" flow { i -> a }",
|
|
8346
|
+
"}",
|
|
8347
|
+
].join("\n");
|
|
8348
|
+
const results = validate(parse(src));
|
|
8349
|
+
expect(results.filter((r) => r.rule === "V7")).toHaveLength(0); // V7 still passes
|
|
8350
|
+
expect(results.filter((r) => r.rule === "V91")).toHaveLength(1); // V91 does not
|
|
8351
|
+
});
|
|
8352
|
+
it("V91: a topology declaring both required fields is clean", () => {
|
|
8353
|
+
const src = [
|
|
8354
|
+
"topology t : [pipeline] {",
|
|
8355
|
+
" meta {",
|
|
8356
|
+
' version: "1.0.0"',
|
|
8357
|
+
' description: "x"',
|
|
8358
|
+
" }",
|
|
8359
|
+
" orchestrator {",
|
|
8360
|
+
" model: opus",
|
|
8361
|
+
" handles: [i]",
|
|
8362
|
+
" }",
|
|
8363
|
+
" agent a {",
|
|
8364
|
+
" model: sonnet",
|
|
8365
|
+
' description: "a"',
|
|
8366
|
+
" }",
|
|
8367
|
+
" action i {",
|
|
8368
|
+
" kind: inline",
|
|
8369
|
+
' description: "in"',
|
|
8370
|
+
" }",
|
|
8371
|
+
" flow { i -> a }",
|
|
8372
|
+
"}",
|
|
8373
|
+
].join("\n");
|
|
8374
|
+
expect(validate(parse(src)).filter((r) => r.rule === "V91")).toHaveLength(0);
|
|
8375
|
+
});
|
|
8376
|
+
it("V91: every shipped example declares its required fields", () => {
|
|
8377
|
+
const dir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../examples");
|
|
8378
|
+
for (const f of readdirSync(dir).filter((x) => x.endsWith(".at"))) {
|
|
8379
|
+
const v91 = validate(parse(readFileSync(resolve(dir, f), "utf8"))).filter((r) => r.rule === "V91");
|
|
8380
|
+
expect(v91, `${f}: ${v91.map((r) => r.message).join("; ")}`).toHaveLength(0);
|
|
8381
|
+
}
|
|
8382
|
+
});
|
|
8383
|
+
it("round-trips the company-brain.at example with no validation errors", () => {
|
|
8384
|
+
const src = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../../examples/company-brain.at"), "utf8");
|
|
8385
|
+
const ast = parse(src);
|
|
8386
|
+
const errors = validate(ast).filter((r) => r.level === "error");
|
|
8387
|
+
expect(errors).toHaveLength(0);
|
|
8388
|
+
});
|
|
8389
|
+
it("round-trips the team-of-N company-brain-team.at example cleanly", () => {
|
|
8390
|
+
const src = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../../examples/company-brain-team.at"), "utf8");
|
|
8391
|
+
const ast = parse(src);
|
|
8392
|
+
const errors = validate(ast).filter((r) => r.level === "error");
|
|
8393
|
+
expect(errors).toHaveLength(0);
|
|
8394
|
+
});
|
|
8395
|
+
it("team example: two distinct custodians of the same brain, with different verbs", () => {
|
|
8396
|
+
const src = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../../examples/company-brain-team.at"), "utf8");
|
|
8397
|
+
const ast = parse(src);
|
|
8398
|
+
const librarian = ast.nodes.find((n) => n.id === "librarian");
|
|
8399
|
+
const auditor = ast.nodes.find((n) => n.id === "auditor");
|
|
8400
|
+
// Both own the same brain store — custody is not exclusive.
|
|
8401
|
+
expect(librarian.custodianOf).toEqual(["brain"]);
|
|
8402
|
+
expect(auditor.custodianOf).toEqual(["brain"]);
|
|
8403
|
+
// But they perform different maintenance verbs (different roles, same layer).
|
|
8404
|
+
expect(librarian.custodianDoes).toEqual(["link", "tag", "index", "dedupe"]);
|
|
8405
|
+
expect(auditor.custodianDoes).toEqual(["heal", "dedupe", "index"]);
|
|
8406
|
+
});
|
|
8407
|
+
});
|
|
7062
8408
|
//# sourceMappingURL=parser.test.js.map
|