agentopology 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +450 -0
- package/dist/analyzer/__tests__/analyzer.test.d.ts +2 -0
- package/dist/analyzer/__tests__/analyzer.test.d.ts.map +1 -0
- package/dist/analyzer/__tests__/analyzer.test.js +258 -0
- package/dist/analyzer/__tests__/analyzer.test.js.map +1 -0
- package/dist/analyzer/index.d.ts +44 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +308 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/bindings/__tests__/bindings.test.d.ts +2 -0
- package/dist/bindings/__tests__/bindings.test.d.ts.map +1 -0
- package/dist/bindings/__tests__/bindings.test.js +2089 -0
- package/dist/bindings/__tests__/bindings.test.js.map +1 -0
- package/dist/bindings/claude-code.d.ts +12 -0
- package/dist/bindings/claude-code.d.ts.map +1 -0
- package/dist/bindings/claude-code.js +2063 -0
- package/dist/bindings/claude-code.js.map +1 -0
- package/dist/bindings/codex.d.ts +18 -0
- package/dist/bindings/codex.d.ts.map +1 -0
- package/dist/bindings/codex.js +1633 -0
- package/dist/bindings/codex.js.map +1 -0
- package/dist/bindings/copilot-cli.d.ts +20 -0
- package/dist/bindings/copilot-cli.d.ts.map +1 -0
- package/dist/bindings/copilot-cli.js +1515 -0
- package/dist/bindings/copilot-cli.js.map +1 -0
- package/dist/bindings/cursor.d.ts +17 -0
- package/dist/bindings/cursor.d.ts.map +1 -0
- package/dist/bindings/cursor.js +1715 -0
- package/dist/bindings/cursor.js.map +1 -0
- package/dist/bindings/gemini-cli.d.ts +18 -0
- package/dist/bindings/gemini-cli.d.ts.map +1 -0
- package/dist/bindings/gemini-cli.js +1327 -0
- package/dist/bindings/gemini-cli.js.map +1 -0
- package/dist/bindings/index.d.ts +20 -0
- package/dist/bindings/index.d.ts.map +1 -0
- package/dist/bindings/index.js +33 -0
- package/dist/bindings/index.js.map +1 -0
- package/dist/bindings/kiro.d.ts +17 -0
- package/dist/bindings/kiro.d.ts.map +1 -0
- package/dist/bindings/kiro.js +1624 -0
- package/dist/bindings/kiro.js.map +1 -0
- package/dist/bindings/openclaw.d.ts +18 -0
- package/dist/bindings/openclaw.d.ts.map +1 -0
- package/dist/bindings/openclaw.js +1578 -0
- package/dist/bindings/openclaw.js.map +1 -0
- package/dist/bindings/types.d.ts +33 -0
- package/dist/bindings/types.d.ts.map +1 -0
- package/dist/bindings/types.js +23 -0
- package/dist/bindings/types.js.map +1 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +679 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/docs/__tests__/docs.test.d.ts +2 -0
- package/dist/docs/__tests__/docs.test.d.ts.map +1 -0
- package/dist/docs/__tests__/docs.test.js +438 -0
- package/dist/docs/__tests__/docs.test.js.map +1 -0
- package/dist/docs/content.d.ts +16 -0
- package/dist/docs/content.d.ts.map +1 -0
- package/dist/docs/content.js +3273 -0
- package/dist/docs/content.js.map +1 -0
- package/dist/docs/index.d.ts +17 -0
- package/dist/docs/index.d.ts.map +1 -0
- package/dist/docs/index.js +117 -0
- package/dist/docs/index.js.map +1 -0
- package/dist/exporters/__tests__/exporters.test.d.ts +2 -0
- package/dist/exporters/__tests__/exporters.test.d.ts.map +1 -0
- package/dist/exporters/__tests__/exporters.test.js +731 -0
- package/dist/exporters/__tests__/exporters.test.js.map +1 -0
- package/dist/exporters/index.d.ts +13 -0
- package/dist/exporters/index.d.ts.map +1 -0
- package/dist/exporters/index.js +18 -0
- package/dist/exporters/index.js.map +1 -0
- package/dist/exporters/json.d.ts +8 -0
- package/dist/exporters/json.d.ts.map +1 -0
- package/dist/exporters/json.js +25 -0
- package/dist/exporters/json.js.map +1 -0
- package/dist/exporters/markdown.d.ts +9 -0
- package/dist/exporters/markdown.d.ts.map +1 -0
- package/dist/exporters/markdown.js +686 -0
- package/dist/exporters/markdown.js.map +1 -0
- package/dist/exporters/mermaid.d.ts +11 -0
- package/dist/exporters/mermaid.d.ts.map +1 -0
- package/dist/exporters/mermaid.js +161 -0
- package/dist/exporters/mermaid.js.map +1 -0
- package/dist/exporters/types.d.ts +22 -0
- package/dist/exporters/types.d.ts.map +1 -0
- package/dist/exporters/types.js +10 -0
- package/dist/exporters/types.js.map +1 -0
- package/dist/import/__tests__/import.test.d.ts +2 -0
- package/dist/import/__tests__/import.test.d.ts.map +1 -0
- package/dist/import/__tests__/import.test.js +781 -0
- package/dist/import/__tests__/import.test.js.map +1 -0
- package/dist/import/claude-code.d.ts +38 -0
- package/dist/import/claude-code.d.ts.map +1 -0
- package/dist/import/claude-code.js +897 -0
- package/dist/import/claude-code.js.map +1 -0
- package/dist/import/index.d.ts +18 -0
- package/dist/import/index.d.ts.map +1 -0
- package/dist/import/index.js +28 -0
- package/dist/import/index.js.map +1 -0
- package/dist/import/serializer.d.ts +11 -0
- package/dist/import/serializer.d.ts.map +1 -0
- package/dist/import/serializer.js +960 -0
- package/dist/import/serializer.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/__tests__/parser.test.d.ts +2 -0
- package/dist/parser/__tests__/parser.test.d.ts.map +1 -0
- package/dist/parser/__tests__/parser.test.js +6514 -0
- package/dist/parser/__tests__/parser.test.js.map +1 -0
- package/dist/parser/ast.d.ts +708 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +12 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/index.d.ts +253 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +1985 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/lexer.d.ts +100 -0
- package/dist/parser/lexer.d.ts.map +1 -0
- package/dist/parser/lexer.js +304 -0
- package/dist/parser/lexer.js.map +1 -0
- package/dist/parser/validator.d.ts +42 -0
- package/dist/parser/validator.d.ts.map +1 -0
- package/dist/parser/validator.js +2568 -0
- package/dist/parser/validator.js.map +1 -0
- package/dist/scaffold/__tests__/scaffold.test.d.ts +7 -0
- package/dist/scaffold/__tests__/scaffold.test.d.ts.map +1 -0
- package/dist/scaffold/__tests__/scaffold.test.js +295 -0
- package/dist/scaffold/__tests__/scaffold.test.js.map +1 -0
- package/dist/scaffold/incremental.d.ts +43 -0
- package/dist/scaffold/incremental.d.ts.map +1 -0
- package/dist/scaffold/incremental.js +185 -0
- package/dist/scaffold/incremental.js.map +1 -0
- package/dist/scaffold/index.d.ts +13 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +13 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/manifest.d.ts +30 -0
- package/dist/scaffold/manifest.d.ts.map +1 -0
- package/dist/scaffold/manifest.js +71 -0
- package/dist/scaffold/manifest.js.map +1 -0
- package/dist/scaffold/merge.d.ts +52 -0
- package/dist/scaffold/merge.d.ts.map +1 -0
- package/dist/scaffold/merge.js +134 -0
- package/dist/scaffold/merge.js.map +1 -0
- package/dist/scaffold/types.d.ts +51 -0
- package/dist/scaffold/types.d.ts.map +1 -0
- package/dist/scaffold/types.js +10 -0
- package/dist/scaffold/types.js.map +1 -0
- package/dist/sync/index.d.ts +34 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +392 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/visualizer/index.d.ts +17 -0
- package/dist/visualizer/index.d.ts.map +1 -0
- package/dist/visualizer/index.js +1976 -0
- package/dist/visualizer/index.js.map +1 -0
- package/examples/code-review.at +167 -0
- package/examples/data-processing.at +296 -0
- package/examples/openclaw-assistant.at +142 -0
- package/examples/scheduled-monitor.at +193 -0
- package/examples/simple-pipeline.at +163 -0
- package/package.json +77 -0
- package/skill/SKILL.md +403 -0
- package/skill/scripts/emit.ts +853 -0
- package/skill/scripts/visualize.ts +89 -0
- package/spec/grammar.md +1466 -0
- package/spec/reserved-keywords.md +147 -0
- package/spec/validation.md +535 -0
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* emit.ts — Generate valid AgenTopology (.at) files from structured JSON input.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* echo '{"topology":...}' | npx tsx emit.ts # stdin -> stdout
|
|
7
|
+
* npx tsx emit.ts input.json # file -> stdout
|
|
8
|
+
* npx tsx emit.ts input.json output.at # file -> file
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
interface TopologyInput {
|
|
19
|
+
topology: {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
description: string;
|
|
23
|
+
patterns: string[];
|
|
24
|
+
foundations?: string[];
|
|
25
|
+
advanced?: string[];
|
|
26
|
+
};
|
|
27
|
+
nodes: Array<NodeInput>;
|
|
28
|
+
edges: Array<EdgeInput>;
|
|
29
|
+
orchestrator?: {
|
|
30
|
+
model: string;
|
|
31
|
+
generates?: string;
|
|
32
|
+
handles: string[];
|
|
33
|
+
outputs?: Record<string, string[]>;
|
|
34
|
+
};
|
|
35
|
+
roles?: Record<string, string>;
|
|
36
|
+
memory?: Record<string, Record<string, any>>;
|
|
37
|
+
triggers?: Array<{ name: string; pattern: string; argument?: string }>;
|
|
38
|
+
hooks?: Array<{ name: string; on: string; matcher?: string; run: string; type?: string }>;
|
|
39
|
+
settings?: { allow?: string[]; deny?: string[]; ask?: string[] };
|
|
40
|
+
mcpServers?: Record<string, { type: string; url?: string; command?: string; args?: string[]; env?: Record<string, string> }>;
|
|
41
|
+
providers?: Array<{ name: string; apiKey?: string; baseUrl?: string; models: string[]; default?: boolean; extra?: Record<string, unknown> }>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface NodeInput {
|
|
45
|
+
id: string;
|
|
46
|
+
type: "agent" | "action" | "gate" | "orchestrator";
|
|
47
|
+
label?: string;
|
|
48
|
+
model?: string;
|
|
49
|
+
permissions?: string;
|
|
50
|
+
prompt?: string;
|
|
51
|
+
phase?: number;
|
|
52
|
+
tools?: string[];
|
|
53
|
+
disallowedTools?: string[];
|
|
54
|
+
reads?: string[];
|
|
55
|
+
writes?: string[];
|
|
56
|
+
outputs?: Record<string, string[]>;
|
|
57
|
+
skip?: string;
|
|
58
|
+
retry?: number;
|
|
59
|
+
isolation?: string;
|
|
60
|
+
invocation?: string;
|
|
61
|
+
behavior?: string;
|
|
62
|
+
role?: string;
|
|
63
|
+
memory?: string;
|
|
64
|
+
skills?: string[];
|
|
65
|
+
mcpServers?: string[];
|
|
66
|
+
background?: boolean;
|
|
67
|
+
hooks?: Array<{ name: string; on: string; matcher?: string; run: string; type?: string; timeout?: number }>;
|
|
68
|
+
// action fields
|
|
69
|
+
kind?: string;
|
|
70
|
+
source?: string;
|
|
71
|
+
commands?: string[];
|
|
72
|
+
description?: string;
|
|
73
|
+
// gate fields
|
|
74
|
+
after?: string;
|
|
75
|
+
before?: string;
|
|
76
|
+
run?: string;
|
|
77
|
+
checks?: string[];
|
|
78
|
+
onFail?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface EdgeInput {
|
|
82
|
+
from: string;
|
|
83
|
+
to: string | string[];
|
|
84
|
+
condition?: string | null;
|
|
85
|
+
maxIterations?: number | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Formatting helpers
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
const I = " "; // 2-space indent
|
|
93
|
+
|
|
94
|
+
/** Wrap a value in quotes. */
|
|
95
|
+
function quoteStr(s: string): string {
|
|
96
|
+
return `"${s}"`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Check if a value looks like an identifier (lowercase, hyphens, starts with letter). */
|
|
100
|
+
function isIdentifier(s: string): boolean {
|
|
101
|
+
return /^[a-z][a-z0-9-]*$/.test(s);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Check if a value looks like a template-var (uppercase). */
|
|
105
|
+
function isTemplateVar(s: string): boolean {
|
|
106
|
+
return /^[A-Z][A-Z0-9_-]*$/.test(s);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Format a value — identifiers and booleans/numbers unquoted, strings quoted. */
|
|
110
|
+
function fmtValue(v: string | number | boolean): string {
|
|
111
|
+
if (typeof v === "number") return String(v);
|
|
112
|
+
if (typeof v === "boolean") return String(v);
|
|
113
|
+
if (v === "true" || v === "false") return v;
|
|
114
|
+
if (isIdentifier(v)) return v;
|
|
115
|
+
if (isTemplateVar(v)) return v;
|
|
116
|
+
// Numbers as strings
|
|
117
|
+
if (/^[0-9]+(\.[0-9]+)?$/.test(v)) return v;
|
|
118
|
+
return quoteStr(v);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Format a list — single line if total width <= maxWidth, multi-line otherwise.
|
|
123
|
+
* maxWidth defaults to 90 (comfortable .at line width).
|
|
124
|
+
*/
|
|
125
|
+
function fmtList(
|
|
126
|
+
items: string[],
|
|
127
|
+
indent: string,
|
|
128
|
+
maxWidth: number = 90,
|
|
129
|
+
formatter: (s: string) => string = fmtValue as (s: string) => string,
|
|
130
|
+
): string {
|
|
131
|
+
if (items.length === 0) return "[]";
|
|
132
|
+
const formatted = items.map(formatter);
|
|
133
|
+
const singleLine = `[${formatted.join(", ")}]`;
|
|
134
|
+
if (singleLine.length <= maxWidth) {
|
|
135
|
+
return singleLine;
|
|
136
|
+
}
|
|
137
|
+
const inner = formatted.map((f) => `${indent}${I}${f}`).join(",\n");
|
|
138
|
+
return `[\n${inner}\n${indent}]`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Format a string list (always quoted). */
|
|
142
|
+
function fmtStringList(items: string[], indent: string, maxWidth: number = 90): string {
|
|
143
|
+
return fmtList(items, indent, maxWidth, quoteStr);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Format a tool item — core tools unquoted, mcp.* unquoted, Bash("x") unquoted. */
|
|
147
|
+
function fmtToolItem(t: string): string {
|
|
148
|
+
// Already formatted like Read, Write, Bash("pattern"), mcp.gitnexus.*
|
|
149
|
+
return t;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Categorize and sort tools: core tools first, then Bash patterns, then mcp.* grouped by server.
|
|
154
|
+
*/
|
|
155
|
+
function sortTools(tools: string[]): string[] {
|
|
156
|
+
const coreOrder = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "Agent"];
|
|
157
|
+
const core: string[] = [];
|
|
158
|
+
const bashPatterns: string[] = [];
|
|
159
|
+
const mcp: string[] = [];
|
|
160
|
+
|
|
161
|
+
for (const t of tools) {
|
|
162
|
+
if (t.startsWith("mcp.")) {
|
|
163
|
+
mcp.push(t);
|
|
164
|
+
} else if (t.startsWith("Bash(")) {
|
|
165
|
+
bashPatterns.push(t);
|
|
166
|
+
} else {
|
|
167
|
+
core.push(t);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Sort core by canonical order
|
|
172
|
+
core.sort((a, b) => {
|
|
173
|
+
const ai = coreOrder.indexOf(a);
|
|
174
|
+
const bi = coreOrder.indexOf(b);
|
|
175
|
+
if (ai === -1 && bi === -1) return a.localeCompare(b);
|
|
176
|
+
if (ai === -1) return 1;
|
|
177
|
+
if (bi === -1) return -1;
|
|
178
|
+
return ai - bi;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Sort MCP by server name, then tool name
|
|
182
|
+
mcp.sort((a, b) => a.localeCompare(b));
|
|
183
|
+
|
|
184
|
+
return [...core, ...bashPatterns, ...mcp];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Format tool list with smart grouping — groups tools by category on the same line.
|
|
189
|
+
* Core tools on one line, then each MCP server's tools on one line.
|
|
190
|
+
*/
|
|
191
|
+
function fmtToolList(tools: string[], indent: string): string {
|
|
192
|
+
if (tools.length === 0) return "[]";
|
|
193
|
+
const sorted = sortTools(tools);
|
|
194
|
+
// If 4 or fewer, single line
|
|
195
|
+
if (sorted.length <= 4) {
|
|
196
|
+
return `[${sorted.map(fmtToolItem).join(", ")}]`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Group: core tools, bash patterns, then MCP tools grouped by server
|
|
200
|
+
const core: string[] = [];
|
|
201
|
+
const bashPatterns: string[] = [];
|
|
202
|
+
const mcpGroups = new Map<string, string[]>();
|
|
203
|
+
|
|
204
|
+
for (const t of sorted) {
|
|
205
|
+
if (t.startsWith("mcp.")) {
|
|
206
|
+
// Group by server: mcp.<server>.* or mcp.<server>.<tool>
|
|
207
|
+
const parts = t.split(".");
|
|
208
|
+
const server = parts[1];
|
|
209
|
+
const group = mcpGroups.get(server) || [];
|
|
210
|
+
group.push(t);
|
|
211
|
+
mcpGroups.set(server, group);
|
|
212
|
+
} else if (t.startsWith("Bash(")) {
|
|
213
|
+
bashPatterns.push(t);
|
|
214
|
+
} else {
|
|
215
|
+
core.push(t);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Build rows: core on one line, each MCP server on one line
|
|
220
|
+
const rows: string[] = [];
|
|
221
|
+
if (core.length > 0) {
|
|
222
|
+
const coreLine = core.join(", ");
|
|
223
|
+
if (bashPatterns.length > 0) {
|
|
224
|
+
rows.push(coreLine + ",");
|
|
225
|
+
rows.push(bashPatterns.join(", "));
|
|
226
|
+
} else {
|
|
227
|
+
rows.push(coreLine);
|
|
228
|
+
}
|
|
229
|
+
} else if (bashPatterns.length > 0) {
|
|
230
|
+
rows.push(bashPatterns.join(", "));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const serverNames = Array.from(mcpGroups.keys()).sort();
|
|
234
|
+
for (const server of serverNames) {
|
|
235
|
+
const serverTools = mcpGroups.get(server)!;
|
|
236
|
+
rows.push(serverTools.join(", "));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check if the whole thing fits on one line
|
|
240
|
+
const allItems = sorted.map(fmtToolItem).join(", ");
|
|
241
|
+
const singleLine = `[${allItems}]`;
|
|
242
|
+
if (singleLine.length <= 90) {
|
|
243
|
+
return singleLine;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Multi-line with grouped rows
|
|
247
|
+
const lines: string[] = [];
|
|
248
|
+
lines.push("[");
|
|
249
|
+
for (let i = 0; i < rows.length; i++) {
|
|
250
|
+
const isLast = i === rows.length - 1;
|
|
251
|
+
const row = rows[i];
|
|
252
|
+
// If row doesn't end with comma and isn't last, add comma
|
|
253
|
+
if (!isLast && !row.endsWith(",")) {
|
|
254
|
+
lines.push(`${indent}${I}${row},`);
|
|
255
|
+
} else {
|
|
256
|
+
lines.push(`${indent}${I}${row}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
lines.push(`${indent}]`);
|
|
260
|
+
return lines.join("\n");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Format path-list — items are quoted except `ticket` keyword. */
|
|
264
|
+
function fmtPathList(items: string[], indent: string): string {
|
|
265
|
+
if (items.length === 0) return "[]";
|
|
266
|
+
const formatted = items.map((item) => (item === "ticket" ? "ticket" : quoteStr(item)));
|
|
267
|
+
const singleLine = `[${formatted.join(", ")}]`;
|
|
268
|
+
if (singleLine.length <= 90) {
|
|
269
|
+
return singleLine;
|
|
270
|
+
}
|
|
271
|
+
const inner = formatted.map((f) => `${indent}${I}${f}`).join(",\n");
|
|
272
|
+
return `[\n${inner}\n${indent}]`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Format typed-map outputs block. */
|
|
276
|
+
function fmtOutputs(outputs: Record<string, string[]>, indent: string): string {
|
|
277
|
+
const entries = Object.entries(outputs);
|
|
278
|
+
if (entries.length === 0) return "{}";
|
|
279
|
+
const lines: string[] = [];
|
|
280
|
+
lines.push("{");
|
|
281
|
+
for (const [key, values] of entries) {
|
|
282
|
+
lines.push(`${indent}${I}${key}: ${values.join(" | ")}`);
|
|
283
|
+
}
|
|
284
|
+
lines.push(`${indent}}`);
|
|
285
|
+
return lines.join("\n");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Section emitters
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
function emitMeta(input: TopologyInput): string[] {
|
|
293
|
+
const lines: string[] = [];
|
|
294
|
+
lines.push(`${I}meta {`);
|
|
295
|
+
lines.push(`${I}${I}version: ${quoteStr(input.topology.version)}`);
|
|
296
|
+
lines.push(`${I}${I}description: ${quoteStr(input.topology.description)}`);
|
|
297
|
+
if (input.topology.foundations && input.topology.foundations.length > 0) {
|
|
298
|
+
lines.push(`${I}${I}foundations: ${fmtList(input.topology.foundations, `${I}${I}`)}`);
|
|
299
|
+
}
|
|
300
|
+
if (input.topology.advanced && input.topology.advanced.length > 0) {
|
|
301
|
+
lines.push(`${I}${I}advanced: ${fmtList(input.topology.advanced, `${I}${I}`)}`);
|
|
302
|
+
}
|
|
303
|
+
lines.push(`${I}}`);
|
|
304
|
+
return lines;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function emitOrchestrator(input: TopologyInput): string[] {
|
|
308
|
+
if (!input.orchestrator) return [];
|
|
309
|
+
const o = input.orchestrator;
|
|
310
|
+
const lines: string[] = [];
|
|
311
|
+
lines.push(`${I}orchestrator {`);
|
|
312
|
+
lines.push(`${I}${I}model: ${o.model}`);
|
|
313
|
+
if (o.generates) {
|
|
314
|
+
lines.push(`${I}${I}generates: ${quoteStr(o.generates)}`);
|
|
315
|
+
}
|
|
316
|
+
lines.push(`${I}${I}handles: ${fmtList(o.handles, `${I}${I}`)}`);
|
|
317
|
+
if (o.outputs && Object.keys(o.outputs).length > 0) {
|
|
318
|
+
lines.push(`${I}${I}outputs: ${fmtOutputs(o.outputs, `${I}${I}`)}`);
|
|
319
|
+
}
|
|
320
|
+
lines.push(`${I}}`);
|
|
321
|
+
return lines;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function emitRoles(input: TopologyInput): string[] {
|
|
325
|
+
if (!input.roles || Object.keys(input.roles).length === 0) return [];
|
|
326
|
+
const lines: string[] = [];
|
|
327
|
+
lines.push(`${I}roles {`);
|
|
328
|
+
for (const [name, desc] of Object.entries(input.roles)) {
|
|
329
|
+
lines.push(`${I}${I}${name}: ${quoteStr(desc)}`);
|
|
330
|
+
}
|
|
331
|
+
lines.push(`${I}}`);
|
|
332
|
+
return lines;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function emitActions(nodes: NodeInput[]): string[] {
|
|
336
|
+
const actions = nodes.filter((n) => n.type === "action");
|
|
337
|
+
if (actions.length === 0) return [];
|
|
338
|
+
const lines: string[] = [];
|
|
339
|
+
lines.push(`${I}# --- Actions (orchestrator-handled steps) ---`);
|
|
340
|
+
for (const action of actions) {
|
|
341
|
+
lines.push("");
|
|
342
|
+
lines.push(`${I}action ${action.id} {`);
|
|
343
|
+
if (action.kind) {
|
|
344
|
+
lines.push(`${I}${I}kind: ${action.kind}`);
|
|
345
|
+
}
|
|
346
|
+
if (action.source) {
|
|
347
|
+
lines.push(`${I}${I}source: ${quoteStr(action.source)}`);
|
|
348
|
+
}
|
|
349
|
+
if (action.commands && action.commands.length > 0) {
|
|
350
|
+
lines.push(`${I}${I}commands: ${fmtList(action.commands, `${I}${I}`)}`);
|
|
351
|
+
}
|
|
352
|
+
if (action.description) {
|
|
353
|
+
lines.push(`${I}${I}description: ${quoteStr(action.description)}`);
|
|
354
|
+
}
|
|
355
|
+
lines.push(`${I}}`);
|
|
356
|
+
}
|
|
357
|
+
return lines;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function emitAgent(node: NodeInput, indent: string): string[] {
|
|
361
|
+
const lines: string[] = [];
|
|
362
|
+
if (node.model) lines.push(`${indent}model: ${node.model}`);
|
|
363
|
+
if (node.permissions && node.permissions !== "auto") {
|
|
364
|
+
lines.push(`${indent}permissions: ${node.permissions}`);
|
|
365
|
+
}
|
|
366
|
+
if (node.prompt) {
|
|
367
|
+
lines.push(`${indent}prompt {`);
|
|
368
|
+
// Indent each line of the prompt content
|
|
369
|
+
for (const promptLine of node.prompt.split("\n")) {
|
|
370
|
+
if (promptLine.trim()) {
|
|
371
|
+
lines.push(`${indent}${I}${promptLine}`);
|
|
372
|
+
} else {
|
|
373
|
+
lines.push("");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
lines.push(`${indent}}`);
|
|
377
|
+
}
|
|
378
|
+
if (node.phase !== undefined && node.phase !== null) {
|
|
379
|
+
lines.push(`${indent}phase: ${node.phase}`);
|
|
380
|
+
}
|
|
381
|
+
if (node.disallowedTools && node.disallowedTools.length > 0) {
|
|
382
|
+
lines.push(`${indent}disallowed-tools: ${fmtToolList(node.disallowedTools, indent)}`);
|
|
383
|
+
}
|
|
384
|
+
if (node.tools && node.tools.length > 0) {
|
|
385
|
+
lines.push(`${indent}tools: ${fmtToolList(node.tools, indent)}`);
|
|
386
|
+
}
|
|
387
|
+
if (node.reads && node.reads.length > 0) {
|
|
388
|
+
lines.push(`${indent}reads: ${fmtPathList(node.reads, indent)}`);
|
|
389
|
+
}
|
|
390
|
+
if (node.writes && node.writes.length > 0) {
|
|
391
|
+
lines.push(`${indent}writes: ${fmtPathList(node.writes, indent)}`);
|
|
392
|
+
}
|
|
393
|
+
if (node.outputs && Object.keys(node.outputs).length > 0) {
|
|
394
|
+
lines.push(`${indent}outputs: ${fmtOutputs(node.outputs, indent)}`);
|
|
395
|
+
}
|
|
396
|
+
if (node.skip) lines.push(`${indent}skip: ${node.skip}`);
|
|
397
|
+
if (node.retry && node.retry > 0) lines.push(`${indent}retry: ${node.retry}`);
|
|
398
|
+
if (node.isolation) lines.push(`${indent}isolation: ${node.isolation}`);
|
|
399
|
+
if (node.behavior && node.behavior !== "blocking") {
|
|
400
|
+
lines.push(`${indent}behavior: ${node.behavior}`);
|
|
401
|
+
}
|
|
402
|
+
if (node.invocation && node.invocation !== "auto") {
|
|
403
|
+
lines.push(`${indent}invocation: ${node.invocation}`);
|
|
404
|
+
}
|
|
405
|
+
if (node.role) lines.push(`${indent}role: ${node.role}`);
|
|
406
|
+
if (node.memory) lines.push(`${indent}memory: ${node.memory}`);
|
|
407
|
+
if (node.skills && node.skills.length > 0) {
|
|
408
|
+
lines.push(`${indent}skills: ${fmtList(node.skills, indent)}`);
|
|
409
|
+
}
|
|
410
|
+
if (node.mcpServers && node.mcpServers.length > 0) {
|
|
411
|
+
lines.push(`${indent}mcp-servers: ${fmtList(node.mcpServers, indent)}`);
|
|
412
|
+
}
|
|
413
|
+
if (node.background === true) lines.push(`${indent}background: true`);
|
|
414
|
+
// Per-agent hooks
|
|
415
|
+
if (node.hooks && node.hooks.length > 0) {
|
|
416
|
+
lines.push(`${indent}hooks {`);
|
|
417
|
+
for (let i = 0; i < node.hooks.length; i++) {
|
|
418
|
+
const h = node.hooks[i];
|
|
419
|
+
if (i > 0) lines.push("");
|
|
420
|
+
lines.push(`${indent}${I}hook ${h.name} {`);
|
|
421
|
+
lines.push(`${indent}${I}${I}on: ${h.on}`);
|
|
422
|
+
if (h.matcher) lines.push(`${indent}${I}${I}matcher: ${quoteStr(h.matcher)}`);
|
|
423
|
+
lines.push(`${indent}${I}${I}run: ${quoteStr(h.run)}`);
|
|
424
|
+
if (h.type) lines.push(`${indent}${I}${I}type: ${h.type}`);
|
|
425
|
+
if (h.timeout && h.timeout !== 600) lines.push(`${indent}${I}${I}timeout: ${h.timeout}`);
|
|
426
|
+
lines.push(`${indent}${I}}`);
|
|
427
|
+
}
|
|
428
|
+
lines.push(`${indent}}`);
|
|
429
|
+
}
|
|
430
|
+
return lines;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function emitAgents(nodes: NodeInput[]): string[] {
|
|
434
|
+
const agents = nodes.filter((n) => n.type === "agent");
|
|
435
|
+
if (agents.length === 0) return [];
|
|
436
|
+
const lines: string[] = [];
|
|
437
|
+
lines.push(`${I}# --- Agents ---`);
|
|
438
|
+
|
|
439
|
+
// Sort agents by phase (undefined phase goes last), then by id
|
|
440
|
+
const sorted = [...agents].sort((a, b) => {
|
|
441
|
+
const ap = a.phase ?? 999;
|
|
442
|
+
const bp = b.phase ?? 999;
|
|
443
|
+
if (ap !== bp) return ap - bp;
|
|
444
|
+
return a.id.localeCompare(b.id);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
for (const agent of sorted) {
|
|
448
|
+
lines.push("");
|
|
449
|
+
lines.push(`${I}agent ${agent.id} {`);
|
|
450
|
+
lines.push(...emitAgent(agent, `${I}${I}`));
|
|
451
|
+
lines.push(`${I}}`);
|
|
452
|
+
}
|
|
453
|
+
return lines;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function emitMemory(memory: Record<string, Record<string, any>> | undefined): string[] {
|
|
457
|
+
if (!memory || Object.keys(memory).length === 0) return [];
|
|
458
|
+
const lines: string[] = [];
|
|
459
|
+
lines.push(`${I}# --- Memory (Blackboard) ---`);
|
|
460
|
+
lines.push("");
|
|
461
|
+
lines.push(`${I}memory {`);
|
|
462
|
+
|
|
463
|
+
const blockNames = Object.keys(memory);
|
|
464
|
+
for (let i = 0; i < blockNames.length; i++) {
|
|
465
|
+
const name = blockNames[i];
|
|
466
|
+
const block = memory[name];
|
|
467
|
+
if (i > 0) lines.push("");
|
|
468
|
+
lines.push(`${I}${I}${name} {`);
|
|
469
|
+
for (const [key, value] of Object.entries(block)) {
|
|
470
|
+
if (Array.isArray(value)) {
|
|
471
|
+
// Check if items look like identifiers or strings
|
|
472
|
+
const allIdentifiers = value.every((v: any) => typeof v === "string" && isIdentifier(v));
|
|
473
|
+
if (allIdentifiers) {
|
|
474
|
+
lines.push(`${I}${I}${I}${key}: ${fmtList(value, `${I}${I}${I}`)}`);
|
|
475
|
+
} else {
|
|
476
|
+
lines.push(`${I}${I}${I}${key}: ${fmtStringList(value, `${I}${I}${I}`)}`);
|
|
477
|
+
}
|
|
478
|
+
} else if (typeof value === "string") {
|
|
479
|
+
if (isIdentifier(value)) {
|
|
480
|
+
lines.push(`${I}${I}${I}${key}: ${value}`);
|
|
481
|
+
} else {
|
|
482
|
+
lines.push(`${I}${I}${I}${key}: ${quoteStr(value)}`);
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
lines.push(`${I}${I}${I}${key}: ${fmtValue(value)}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
lines.push(`${I}${I}}`);
|
|
489
|
+
}
|
|
490
|
+
lines.push(`${I}}`);
|
|
491
|
+
return lines;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function emitFlow(edges: EdgeInput[]): string[] {
|
|
495
|
+
if (edges.length === 0) return [];
|
|
496
|
+
const lines: string[] = [];
|
|
497
|
+
lines.push(`${I}# --- Flow ---`);
|
|
498
|
+
lines.push("");
|
|
499
|
+
lines.push(`${I}flow {`);
|
|
500
|
+
|
|
501
|
+
// Build chain-merged flow statements.
|
|
502
|
+
// Detect sequences of unconditional edges (a->b, b->c) and merge into a->b->c.
|
|
503
|
+
// Only merge when both edges are unconditional, single-target, and the intermediate
|
|
504
|
+
// node has exactly one unconditional incoming and one unconditional outgoing edge.
|
|
505
|
+
type FlowStmt = { chain: string[]; condition?: string; maxIterations?: number };
|
|
506
|
+
const stmts: FlowStmt[] = [];
|
|
507
|
+
|
|
508
|
+
// Index: for each node, which unconditional edges go out?
|
|
509
|
+
const unconditionalOutgoing = new Map<string, EdgeInput[]>();
|
|
510
|
+
const unconditionalIncoming = new Map<string, EdgeInput[]>();
|
|
511
|
+
for (const e of edges) {
|
|
512
|
+
if (!e.condition && !Array.isArray(e.to)) {
|
|
513
|
+
const list = unconditionalOutgoing.get(e.from) || [];
|
|
514
|
+
list.push(e);
|
|
515
|
+
unconditionalOutgoing.set(e.from, list);
|
|
516
|
+
const inList = unconditionalIncoming.get(e.to as string) || [];
|
|
517
|
+
inList.push(e);
|
|
518
|
+
unconditionalIncoming.set(e.to as string, inList);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Track which edges have been consumed by chaining
|
|
523
|
+
const consumed = new Set<number>();
|
|
524
|
+
|
|
525
|
+
for (let i = 0; i < edges.length; i++) {
|
|
526
|
+
if (consumed.has(i)) continue;
|
|
527
|
+
const edge = edges[i];
|
|
528
|
+
|
|
529
|
+
// Try to build a chain starting from this edge (only for unconditional single-target edges)
|
|
530
|
+
if (!edge.condition && !edge.maxIterations && !Array.isArray(edge.to)) {
|
|
531
|
+
const chain = [edge.from, edge.to as string];
|
|
532
|
+
consumed.add(i);
|
|
533
|
+
|
|
534
|
+
// Extend chain: look for the next edge starting from chain's tail
|
|
535
|
+
let extended = true;
|
|
536
|
+
while (extended) {
|
|
537
|
+
extended = false;
|
|
538
|
+
const tail = chain[chain.length - 1];
|
|
539
|
+
// Only chain if tail has exactly one unconditional outgoing and one unconditional incoming
|
|
540
|
+
const outEdges = unconditionalOutgoing.get(tail) || [];
|
|
541
|
+
const inEdges = unconditionalIncoming.get(tail) || [];
|
|
542
|
+
if (outEdges.length === 1 && inEdges.length === 1) {
|
|
543
|
+
const nextEdge = outEdges[0];
|
|
544
|
+
const nextIdx = edges.indexOf(nextEdge);
|
|
545
|
+
if (nextIdx >= 0 && !consumed.has(nextIdx) && !nextEdge.condition && !nextEdge.maxIterations && !Array.isArray(nextEdge.to)) {
|
|
546
|
+
chain.push(nextEdge.to as string);
|
|
547
|
+
consumed.add(nextIdx);
|
|
548
|
+
extended = true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
stmts.push({ chain });
|
|
554
|
+
} else {
|
|
555
|
+
// Conditional or fan-out edge — emit directly
|
|
556
|
+
consumed.add(i);
|
|
557
|
+
const to = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
558
|
+
stmts.push({
|
|
559
|
+
chain: [edge.from, ...(Array.isArray(edge.to) ? [] : to)],
|
|
560
|
+
condition: edge.condition || undefined,
|
|
561
|
+
maxIterations: edge.maxIterations || undefined,
|
|
562
|
+
// For fan-out, store the array
|
|
563
|
+
...(Array.isArray(edge.to) ? { _fanOut: edge.to } : {}),
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Format flow lines
|
|
569
|
+
const flowLines: Array<{ text: string; attrs: string }> = [];
|
|
570
|
+
for (const stmt of stmts) {
|
|
571
|
+
const s = stmt as any;
|
|
572
|
+
let base: string;
|
|
573
|
+
if (s._fanOut) {
|
|
574
|
+
base = `${stmt.chain[0]} -> [${s._fanOut.join(", ")}]`;
|
|
575
|
+
} else {
|
|
576
|
+
base = stmt.chain.join(" -> ");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const attrs: string[] = [];
|
|
580
|
+
if (stmt.condition) {
|
|
581
|
+
attrs.push(`when ${stmt.condition}`);
|
|
582
|
+
}
|
|
583
|
+
if (stmt.maxIterations) {
|
|
584
|
+
attrs.push(`max ${stmt.maxIterations}`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
flowLines.push({ text: base, attrs: attrs.length > 0 ? `[${attrs.join(", ")}]` : "" });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Calculate alignment column for conditions
|
|
591
|
+
const condLines = flowLines.filter((f) => f.attrs);
|
|
592
|
+
const maxBase = condLines.length > 0 ? Math.max(...condLines.map((f) => f.text.length)) : 0;
|
|
593
|
+
const alignCol = maxBase + 2;
|
|
594
|
+
|
|
595
|
+
for (const fl of flowLines) {
|
|
596
|
+
if (fl.attrs) {
|
|
597
|
+
const padded = fl.text.padEnd(alignCol);
|
|
598
|
+
lines.push(`${I}${I}${padded}${fl.attrs}`);
|
|
599
|
+
} else {
|
|
600
|
+
lines.push(`${I}${I}${fl.text}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
lines.push(`${I}}`);
|
|
605
|
+
return lines;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function emitGates(nodes: NodeInput[]): string[] {
|
|
609
|
+
const gates = nodes.filter((n) => n.type === "gate");
|
|
610
|
+
if (gates.length === 0) return [];
|
|
611
|
+
const lines: string[] = [];
|
|
612
|
+
lines.push(`${I}# --- Gates ---`);
|
|
613
|
+
lines.push("");
|
|
614
|
+
lines.push(`${I}gates {`);
|
|
615
|
+
for (let i = 0; i < gates.length; i++) {
|
|
616
|
+
const gate = gates[i];
|
|
617
|
+
if (i > 0) lines.push("");
|
|
618
|
+
lines.push(`${I}${I}gate ${gate.id} {`);
|
|
619
|
+
if (gate.after) lines.push(`${I}${I}${I}after: ${gate.after}`);
|
|
620
|
+
if (gate.before) lines.push(`${I}${I}${I}before: ${gate.before}`);
|
|
621
|
+
if (gate.run) lines.push(`${I}${I}${I}run: ${quoteStr(gate.run)}`);
|
|
622
|
+
if (gate.checks && gate.checks.length > 0) {
|
|
623
|
+
lines.push(`${I}${I}${I}checks: ${fmtList(gate.checks, `${I}${I}${I}`)}`);
|
|
624
|
+
}
|
|
625
|
+
if (gate.retry && gate.retry > 0) {
|
|
626
|
+
lines.push(`${I}${I}${I}retry: ${gate.retry}`);
|
|
627
|
+
}
|
|
628
|
+
if (gate.onFail) {
|
|
629
|
+
lines.push(`${I}${I}${I}on-fail: ${gate.onFail}`);
|
|
630
|
+
}
|
|
631
|
+
if (gate.behavior && gate.behavior !== "blocking") {
|
|
632
|
+
lines.push(`${I}${I}${I}behavior: ${gate.behavior}`);
|
|
633
|
+
}
|
|
634
|
+
lines.push(`${I}${I}}`);
|
|
635
|
+
}
|
|
636
|
+
lines.push(`${I}}`);
|
|
637
|
+
return lines;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function emitTriggers(triggers: TopologyInput["triggers"]): string[] {
|
|
641
|
+
if (!triggers || triggers.length === 0) return [];
|
|
642
|
+
const lines: string[] = [];
|
|
643
|
+
lines.push(`${I}# --- Triggers ---`);
|
|
644
|
+
lines.push("");
|
|
645
|
+
lines.push(`${I}triggers {`);
|
|
646
|
+
for (let i = 0; i < triggers.length; i++) {
|
|
647
|
+
const t = triggers[i];
|
|
648
|
+
if (i > 0) lines.push("");
|
|
649
|
+
lines.push(`${I}${I}command ${t.name} {`);
|
|
650
|
+
lines.push(`${I}${I}${I}pattern: ${quoteStr(t.pattern)}`);
|
|
651
|
+
if (t.argument) {
|
|
652
|
+
lines.push(`${I}${I}${I}argument: ${t.argument}`);
|
|
653
|
+
}
|
|
654
|
+
lines.push(`${I}${I}}`);
|
|
655
|
+
}
|
|
656
|
+
lines.push(`${I}}`);
|
|
657
|
+
return lines;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function emitHooks(hooks: TopologyInput["hooks"]): string[] {
|
|
661
|
+
if (!hooks || hooks.length === 0) return [];
|
|
662
|
+
const lines: string[] = [];
|
|
663
|
+
lines.push(`${I}# --- Hooks ---`);
|
|
664
|
+
lines.push("");
|
|
665
|
+
lines.push(`${I}hooks {`);
|
|
666
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
667
|
+
const h = hooks[i];
|
|
668
|
+
if (i > 0) lines.push("");
|
|
669
|
+
lines.push(`${I}${I}hook ${h.name} {`);
|
|
670
|
+
lines.push(`${I}${I}${I}on: ${h.on}`);
|
|
671
|
+
if (h.matcher) lines.push(`${I}${I}${I}matcher: ${quoteStr(h.matcher)}`);
|
|
672
|
+
lines.push(`${I}${I}${I}run: ${quoteStr(h.run)}`);
|
|
673
|
+
if (h.type) lines.push(`${I}${I}${I}type: ${h.type}`);
|
|
674
|
+
lines.push(`${I}${I}}`);
|
|
675
|
+
}
|
|
676
|
+
lines.push(`${I}}`);
|
|
677
|
+
return lines;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function emitSettings(settings: TopologyInput["settings"]): string[] {
|
|
681
|
+
if (!settings) return [];
|
|
682
|
+
const has = (settings.allow && settings.allow.length > 0)
|
|
683
|
+
|| (settings.deny && settings.deny.length > 0)
|
|
684
|
+
|| (settings.ask && settings.ask.length > 0);
|
|
685
|
+
if (!has) return [];
|
|
686
|
+
const lines: string[] = [];
|
|
687
|
+
lines.push(`${I}# --- Settings ---`);
|
|
688
|
+
lines.push("");
|
|
689
|
+
lines.push(`${I}settings {`);
|
|
690
|
+
if (settings.allow && settings.allow.length > 0) {
|
|
691
|
+
lines.push(`${I}${I}allow: ${fmtStringList(settings.allow, `${I}${I}`)}`);
|
|
692
|
+
}
|
|
693
|
+
if (settings.deny !== undefined) {
|
|
694
|
+
if (settings.deny.length === 0) {
|
|
695
|
+
lines.push(`${I}${I}deny: []`);
|
|
696
|
+
} else {
|
|
697
|
+
lines.push(`${I}${I}deny: ${fmtStringList(settings.deny, `${I}${I}`)}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (settings.ask !== undefined) {
|
|
701
|
+
if (settings.ask.length === 0) {
|
|
702
|
+
lines.push(`${I}${I}ask: []`);
|
|
703
|
+
} else {
|
|
704
|
+
lines.push(`${I}${I}ask: ${fmtStringList(settings.ask, `${I}${I}`)}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
lines.push(`${I}}`);
|
|
708
|
+
return lines;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function emitMcpServers(servers: TopologyInput["mcpServers"]): string[] {
|
|
712
|
+
if (!servers || Object.keys(servers).length === 0) return [];
|
|
713
|
+
const lines: string[] = [];
|
|
714
|
+
lines.push(`${I}# --- MCP Servers ---`);
|
|
715
|
+
lines.push("");
|
|
716
|
+
lines.push(`${I}mcp-servers {`);
|
|
717
|
+
const names = Object.keys(servers);
|
|
718
|
+
for (let i = 0; i < names.length; i++) {
|
|
719
|
+
const name = names[i];
|
|
720
|
+
const srv = servers[name];
|
|
721
|
+
if (i > 0) lines.push("");
|
|
722
|
+
lines.push(`${I}${I}${name} {`);
|
|
723
|
+
lines.push(`${I}${I}${I}type: ${srv.type}`);
|
|
724
|
+
if (srv.url) lines.push(`${I}${I}${I}url: ${quoteStr(srv.url)}`);
|
|
725
|
+
if (srv.command) lines.push(`${I}${I}${I}command: ${quoteStr(srv.command)}`);
|
|
726
|
+
if (srv.args && srv.args.length > 0) {
|
|
727
|
+
lines.push(`${I}${I}${I}args: ${fmtStringList(srv.args, `${I}${I}${I}`)}`);
|
|
728
|
+
}
|
|
729
|
+
if (srv.env && Object.keys(srv.env).length > 0) {
|
|
730
|
+
lines.push(`${I}${I}${I}env: {`);
|
|
731
|
+
for (const [k, v] of Object.entries(srv.env)) {
|
|
732
|
+
lines.push(`${I}${I}${I}${I}${k}: ${quoteStr(v)}`);
|
|
733
|
+
}
|
|
734
|
+
lines.push(`${I}${I}${I}}`);
|
|
735
|
+
}
|
|
736
|
+
lines.push(`${I}${I}}`);
|
|
737
|
+
}
|
|
738
|
+
lines.push(`${I}}`);
|
|
739
|
+
return lines;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function emitProviders(providers: TopologyInput["providers"]): string[] {
|
|
743
|
+
if (!providers || providers.length === 0) return [];
|
|
744
|
+
const lines: string[] = [];
|
|
745
|
+
lines.push(`${I}# --- Providers ---`);
|
|
746
|
+
lines.push("");
|
|
747
|
+
lines.push(`${I}providers {`);
|
|
748
|
+
for (let i = 0; i < providers.length; i++) {
|
|
749
|
+
const p = providers[i];
|
|
750
|
+
if (i > 0) lines.push("");
|
|
751
|
+
lines.push(`${I}${I}${p.name} {`);
|
|
752
|
+
if (p.apiKey) lines.push(`${I}${I}${I}api-key: ${quoteStr(p.apiKey)}`);
|
|
753
|
+
if (p.baseUrl) lines.push(`${I}${I}${I}base-url: ${quoteStr(p.baseUrl)}`);
|
|
754
|
+
if (p.models && p.models.length > 0) {
|
|
755
|
+
lines.push(`${I}${I}${I}models: ${fmtList(p.models, `${I}${I}${I}`)}`);
|
|
756
|
+
}
|
|
757
|
+
if (p.default === true) lines.push(`${I}${I}${I}default: true`);
|
|
758
|
+
if (p.extra) {
|
|
759
|
+
for (const [key, value] of Object.entries(p.extra)) {
|
|
760
|
+
lines.push(`${I}${I}${I}${key}: ${fmtValue(value as string | number | boolean)}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
lines.push(`${I}${I}}`);
|
|
764
|
+
}
|
|
765
|
+
lines.push(`${I}}`);
|
|
766
|
+
return lines;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ---------------------------------------------------------------------------
|
|
770
|
+
// Main emitter
|
|
771
|
+
// ---------------------------------------------------------------------------
|
|
772
|
+
|
|
773
|
+
function emit(input: TopologyInput): string {
|
|
774
|
+
const sections: string[][] = [];
|
|
775
|
+
|
|
776
|
+
// Header comment
|
|
777
|
+
const header = [
|
|
778
|
+
`# ${input.topology.name}.at — ${input.topology.description}`,
|
|
779
|
+
`# AgenTopology v1.0`,
|
|
780
|
+
];
|
|
781
|
+
|
|
782
|
+
// Topology declaration
|
|
783
|
+
const patterns = input.topology.patterns.join(", ");
|
|
784
|
+
const topOpen = `topology ${input.topology.name} : [${patterns}] {`;
|
|
785
|
+
|
|
786
|
+
// Build sections in canonical order
|
|
787
|
+
sections.push(emitMeta(input));
|
|
788
|
+
sections.push(emitOrchestrator(input));
|
|
789
|
+
sections.push(emitRoles(input));
|
|
790
|
+
sections.push(emitActions(input.nodes));
|
|
791
|
+
sections.push(emitAgents(input.nodes));
|
|
792
|
+
sections.push(emitMemory(input.memory));
|
|
793
|
+
sections.push(emitFlow(input.edges));
|
|
794
|
+
sections.push(emitGates(input.nodes));
|
|
795
|
+
sections.push(emitTriggers(input.triggers));
|
|
796
|
+
sections.push(emitHooks(input.hooks));
|
|
797
|
+
sections.push(emitProviders(input.providers));
|
|
798
|
+
sections.push(emitSettings(input.settings));
|
|
799
|
+
sections.push(emitMcpServers(input.mcpServers));
|
|
800
|
+
|
|
801
|
+
// Assemble
|
|
802
|
+
const parts: string[] = [];
|
|
803
|
+
parts.push(header.join("\n"));
|
|
804
|
+
parts.push("");
|
|
805
|
+
parts.push(topOpen);
|
|
806
|
+
|
|
807
|
+
for (const section of sections) {
|
|
808
|
+
if (section.length === 0) continue;
|
|
809
|
+
parts.push("");
|
|
810
|
+
parts.push(section.join("\n"));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
parts.push("}");
|
|
814
|
+
parts.push(""); // trailing newline
|
|
815
|
+
|
|
816
|
+
return parts.join("\n");
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
// CLI entry point
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
|
|
823
|
+
async function main(): Promise<void> {
|
|
824
|
+
let jsonStr: string;
|
|
825
|
+
|
|
826
|
+
if (process.argv[2]) {
|
|
827
|
+
// Read from file
|
|
828
|
+
const inputPath = path.resolve(process.argv[2]);
|
|
829
|
+
jsonStr = fs.readFileSync(inputPath, "utf-8");
|
|
830
|
+
} else {
|
|
831
|
+
// Read from stdin
|
|
832
|
+
const chunks: Buffer[] = [];
|
|
833
|
+
for await (const chunk of process.stdin) {
|
|
834
|
+
chunks.push(chunk as Buffer);
|
|
835
|
+
}
|
|
836
|
+
jsonStr = Buffer.concat(chunks).toString("utf-8");
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const input: TopologyInput = JSON.parse(jsonStr);
|
|
840
|
+
const output = emit(input);
|
|
841
|
+
|
|
842
|
+
if (process.argv[3]) {
|
|
843
|
+
const outputPath = path.resolve(process.argv[3]);
|
|
844
|
+
fs.writeFileSync(outputPath, output, "utf-8");
|
|
845
|
+
} else {
|
|
846
|
+
process.stdout.write(output);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
main().catch((err) => {
|
|
851
|
+
console.error("emit.ts error:", err.message);
|
|
852
|
+
process.exit(1);
|
|
853
|
+
});
|