@agent-relay/sdk 2.3.14 → 2.3.15
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 +68 -838
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/__tests__/contract-fixtures.test.d.ts +2 -0
- package/dist/__tests__/contract-fixtures.test.d.ts.map +1 -0
- package/dist/__tests__/contract-fixtures.test.js +85 -0
- package/dist/__tests__/contract-fixtures.test.js.map +1 -0
- package/dist/__tests__/facade.test.d.ts +2 -0
- package/dist/__tests__/facade.test.d.ts.map +1 -0
- package/dist/__tests__/facade.test.js +305 -0
- package/dist/__tests__/facade.test.js.map +1 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +169 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/pty.test.d.ts +2 -0
- package/dist/__tests__/pty.test.d.ts.map +1 -0
- package/dist/__tests__/pty.test.js +20 -0
- package/dist/__tests__/pty.test.js.map +1 -0
- package/dist/__tests__/quickstart.test.d.ts +2 -0
- package/dist/__tests__/quickstart.test.d.ts.map +1 -0
- package/dist/__tests__/quickstart.test.js +176 -0
- package/dist/__tests__/quickstart.test.js.map +1 -0
- package/dist/__tests__/spawn-from-env.test.d.ts +2 -0
- package/dist/__tests__/spawn-from-env.test.d.ts.map +1 -0
- package/dist/__tests__/spawn-from-env.test.js +206 -0
- package/dist/__tests__/spawn-from-env.test.js.map +1 -0
- package/dist/__tests__/unit.test.d.ts +2 -0
- package/dist/__tests__/unit.test.d.ts.map +1 -0
- package/dist/__tests__/unit.test.js +347 -0
- package/dist/__tests__/unit.test.js.map +1 -0
- package/dist/browser.d.ts +16 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +19 -0
- package/dist/browser.js.map +1 -0
- package/dist/client.d.ts +140 -526
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +430 -1509
- package/dist/client.js.map +1 -1
- package/dist/consensus-helpers.d.ts +103 -0
- package/dist/consensus-helpers.d.ts.map +1 -0
- package/dist/consensus-helpers.js +147 -0
- package/dist/consensus-helpers.js.map +1 -0
- package/dist/consensus.d.ts +72 -0
- package/dist/consensus.d.ts.map +1 -0
- package/dist/consensus.js +378 -0
- package/dist/consensus.js.map +1 -0
- package/dist/examples/demo.d.ts +2 -0
- package/dist/examples/demo.d.ts.map +1 -0
- package/dist/examples/demo.js +63 -0
- package/dist/examples/demo.js.map +1 -0
- package/dist/examples/example.d.ts +2 -0
- package/dist/examples/example.d.ts.map +1 -0
- package/dist/examples/example.js +80 -0
- package/dist/examples/example.js.map +1 -0
- package/dist/examples/quickstart.d.ts +2 -0
- package/dist/examples/quickstart.d.ts.map +1 -0
- package/dist/examples/quickstart.js +56 -0
- package/dist/examples/quickstart.js.map +1 -0
- package/dist/examples/ralph-loop.d.ts +2 -0
- package/dist/examples/ralph-loop.d.ts.map +1 -0
- package/dist/examples/ralph-loop.js +281 -0
- package/dist/examples/ralph-loop.js.map +1 -0
- package/dist/examples/workflow-superiority.d.ts +32 -0
- package/dist/examples/workflow-superiority.d.ts.map +1 -0
- package/dist/examples/workflow-superiority.js +1421 -0
- package/dist/examples/workflow-superiority.js.map +1 -0
- package/dist/index.d.ts +13 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -26
- package/dist/index.js.map +1 -1
- package/dist/logs.d.ts +70 -25
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +238 -42
- package/dist/logs.js.map +1 -1
- package/dist/models.d.ts +9 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +17 -0
- package/dist/models.js.map +1 -0
- package/dist/protocol.d.ts +366 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +2 -0
- package/dist/protocol.js.map +1 -0
- package/dist/pty.d.ts +8 -0
- package/dist/pty.d.ts.map +1 -0
- package/dist/pty.js +26 -0
- package/dist/pty.js.map +1 -0
- package/dist/relay-adapter.d.ts +139 -0
- package/dist/relay-adapter.d.ts.map +1 -0
- package/dist/relay-adapter.js +210 -0
- package/dist/relay-adapter.js.map +1 -0
- package/dist/relay.d.ts +304 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +910 -0
- package/dist/relay.js.map +1 -0
- package/dist/shadow.d.ts +101 -0
- package/dist/shadow.d.ts.map +1 -0
- package/dist/shadow.js +174 -0
- package/dist/shadow.js.map +1 -0
- package/dist/spawn-from-env.d.ts +77 -0
- package/dist/spawn-from-env.d.ts.map +1 -0
- package/dist/spawn-from-env.js +172 -0
- package/dist/spawn-from-env.js.map +1 -0
- package/dist/workflows/barrier.d.ts +72 -0
- package/dist/workflows/barrier.d.ts.map +1 -0
- package/dist/workflows/barrier.js +162 -0
- package/dist/workflows/barrier.js.map +1 -0
- package/dist/workflows/builder.d.ts +114 -0
- package/dist/workflows/builder.d.ts.map +1 -0
- package/dist/workflows/builder.js +201 -0
- package/dist/workflows/builder.js.map +1 -0
- package/dist/workflows/cli.d.ts +11 -0
- package/dist/workflows/cli.d.ts.map +1 -0
- package/dist/workflows/cli.js +144 -0
- package/dist/workflows/cli.js.map +1 -0
- package/dist/workflows/coordinator.d.ts +73 -0
- package/dist/workflows/coordinator.d.ts.map +1 -0
- package/dist/workflows/coordinator.js +647 -0
- package/dist/workflows/coordinator.js.map +1 -0
- package/dist/workflows/custom-steps.d.ts +73 -0
- package/dist/workflows/custom-steps.d.ts.map +1 -0
- package/dist/workflows/custom-steps.js +321 -0
- package/dist/workflows/custom-steps.js.map +1 -0
- package/dist/workflows/dry-run-format.d.ts +6 -0
- package/dist/workflows/dry-run-format.d.ts.map +1 -0
- package/dist/workflows/dry-run-format.js +68 -0
- package/dist/workflows/dry-run-format.js.map +1 -0
- package/dist/workflows/file-db.d.ts +33 -0
- package/dist/workflows/file-db.d.ts.map +1 -0
- package/dist/workflows/file-db.js +108 -0
- package/dist/workflows/file-db.js.map +1 -0
- package/dist/workflows/index.d.ts +15 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +15 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/memory-db.d.ts +17 -0
- package/dist/workflows/memory-db.d.ts.map +1 -0
- package/dist/workflows/memory-db.js +33 -0
- package/dist/workflows/memory-db.js.map +1 -0
- package/dist/workflows/run.d.ts +38 -0
- package/dist/workflows/run.d.ts.map +1 -0
- package/dist/workflows/run.js +25 -0
- package/dist/workflows/run.js.map +1 -0
- package/dist/workflows/runner.d.ts +320 -0
- package/dist/workflows/runner.d.ts.map +1 -0
- package/dist/workflows/runner.js +2821 -0
- package/dist/workflows/runner.js.map +1 -0
- package/dist/workflows/state.d.ts +77 -0
- package/dist/workflows/state.d.ts.map +1 -0
- package/dist/workflows/state.js +140 -0
- package/dist/workflows/state.js.map +1 -0
- package/dist/workflows/templates.d.ts +47 -0
- package/dist/workflows/templates.d.ts.map +1 -0
- package/dist/workflows/templates.js +405 -0
- package/dist/workflows/templates.js.map +1 -0
- package/dist/workflows/trajectory.d.ts +87 -0
- package/dist/workflows/trajectory.d.ts.map +1 -0
- package/dist/workflows/trajectory.js +441 -0
- package/dist/workflows/trajectory.js.map +1 -0
- package/dist/workflows/types.d.ts +306 -0
- package/dist/workflows/types.d.ts.map +1 -0
- package/dist/workflows/types.js +23 -0
- package/dist/workflows/types.js.map +1 -0
- package/dist/workflows/validator.d.ts +11 -0
- package/dist/workflows/validator.d.ts.map +1 -0
- package/dist/workflows/validator.js +128 -0
- package/dist/workflows/validator.js.map +1 -0
- package/package.json +59 -53
- package/dist/discovery.d.ts +0 -10
- package/dist/discovery.d.ts.map +0 -1
- package/dist/discovery.js +0 -22
- package/dist/discovery.js.map +0 -1
- package/dist/errors.d.ts +0 -9
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -9
- package/dist/errors.js.map +0 -1
- package/dist/protocol/index.d.ts +0 -8
- package/dist/protocol/index.d.ts.map +0 -1
- package/dist/protocol/index.js +0 -8
- package/dist/protocol/index.js.map +0 -1
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm Coordinator — pattern selection, agent topology, and workflow lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates workflow runs: picks the right swarm pattern (or auto-selects),
|
|
5
|
+
* resolves agent topology from the config, and drives the run through its
|
|
6
|
+
* lifecycle states (pending → running → completed / failed / cancelled).
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
9
|
+
import { EventEmitter } from 'node:events';
|
|
10
|
+
// ── Pattern auto-selection ──────────────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Mapping used when auto-selecting a pattern from config heuristics.
|
|
13
|
+
* The coordinator checks the config shape and picks the best match.
|
|
14
|
+
*/
|
|
15
|
+
const PATTERN_HEURISTICS = [
|
|
16
|
+
// ── Dependency-based patterns (highest priority) ──────────────────────
|
|
17
|
+
{
|
|
18
|
+
test: (c) => Array.isArray(c.workflows) &&
|
|
19
|
+
c.workflows.some((w) => w.steps.some((s) => s.dependsOn?.length)),
|
|
20
|
+
pattern: 'dag',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
test: (c) => c.coordination?.consensusStrategy !== undefined,
|
|
24
|
+
pattern: 'consensus',
|
|
25
|
+
},
|
|
26
|
+
// ── Specific role-based patterns (check before generic hub patterns) ──
|
|
27
|
+
{
|
|
28
|
+
// Map-reduce: requires BOTH mapper AND reducer roles
|
|
29
|
+
test: (c) => c.agents.some((a) => a.role === 'mapper') && c.agents.some((a) => a.role === 'reducer'),
|
|
30
|
+
pattern: 'map-reduce',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
// Red-team: requires BOTH attacker/red-team AND defender/blue-team
|
|
34
|
+
test: (c) => c.agents.some((a) => a.role === 'attacker' || a.role === 'red-team') &&
|
|
35
|
+
c.agents.some((a) => a.role === 'defender' || a.role === 'blue-team'),
|
|
36
|
+
pattern: 'red-team',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
// Reflection: requires critic role (not just reviewer, which is too common)
|
|
40
|
+
test: (c) => c.agents.some((a) => a.role === 'critic'),
|
|
41
|
+
pattern: 'reflection',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
// Escalation: has tier-N roles
|
|
45
|
+
test: (c) => c.agents.some((a) => a.role?.startsWith('tier-')),
|
|
46
|
+
pattern: 'escalation',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
// Auction: has auctioneer role
|
|
50
|
+
test: (c) => c.agents.some((a) => a.role === 'auctioneer'),
|
|
51
|
+
pattern: 'auction',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
// Saga: has saga-orchestrator or compensate-handler roles
|
|
55
|
+
test: (c) => c.agents.some((a) => a.role === 'saga-orchestrator' || a.role === 'compensate-handler'),
|
|
56
|
+
pattern: 'saga',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
// Circuit-breaker: has fallback or backup roles
|
|
60
|
+
test: (c) => c.agents.some((a) => a.role === 'fallback' || a.role === 'backup' || a.role === 'primary'),
|
|
61
|
+
pattern: 'circuit-breaker',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
// Blackboard: has blackboard or shared-workspace role
|
|
65
|
+
test: (c) => c.agents.some((a) => a.role === 'blackboard' || a.role === 'shared-workspace'),
|
|
66
|
+
pattern: 'blackboard',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
// Swarm: has hive-mind or swarm-agent roles
|
|
70
|
+
test: (c) => c.agents.some((a) => a.role === 'hive-mind' || a.role === 'swarm-agent'),
|
|
71
|
+
pattern: 'swarm',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
// Verifier: has verifier role
|
|
75
|
+
test: (c) => c.agents.some((a) => a.role === 'verifier'),
|
|
76
|
+
pattern: 'verifier',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
// Supervisor: has supervisor role
|
|
80
|
+
test: (c) => c.agents.some((a) => a.role === 'supervisor'),
|
|
81
|
+
pattern: 'supervisor',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
// Review-loop: implementer + multiple reviewers (code review with feedback loop)
|
|
85
|
+
test: (c) => {
|
|
86
|
+
const hasImplementer = c.agents.some((a) => a.role?.toLowerCase().includes('implement') || a.name.toLowerCase().includes('implement'));
|
|
87
|
+
const reviewerCount = c.agents.filter((a) => a.role?.toLowerCase().includes('reviewer') || a.name.toLowerCase().includes('reviewer')).length;
|
|
88
|
+
return hasImplementer && reviewerCount >= 2;
|
|
89
|
+
},
|
|
90
|
+
pattern: 'review-loop',
|
|
91
|
+
},
|
|
92
|
+
// ── Generic hub-based patterns ────────────────────────────────────────
|
|
93
|
+
{
|
|
94
|
+
test: (c) => c.agents.length > 3 && c.agents.some((a) => a.role === 'lead'),
|
|
95
|
+
pattern: 'hierarchical',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
test: (c) => c.agents.some((a) => a.role === 'hub' || a.role === 'coordinator'),
|
|
99
|
+
pattern: 'hub-spoke',
|
|
100
|
+
},
|
|
101
|
+
// ── Structural patterns ───────────────────────────────────────────────
|
|
102
|
+
{
|
|
103
|
+
test: (c) => Array.isArray(c.workflows) &&
|
|
104
|
+
c.workflows.some((w) => {
|
|
105
|
+
// Filter to only agent steps
|
|
106
|
+
const names = w.steps.filter((s) => s.agent).map((s) => s.agent);
|
|
107
|
+
return new Set(names).size === names.length && names.length > 2;
|
|
108
|
+
}),
|
|
109
|
+
pattern: 'pipeline',
|
|
110
|
+
},
|
|
111
|
+
// ── Default fallback ──────────────────────────────────────────────────
|
|
112
|
+
{
|
|
113
|
+
test: () => true,
|
|
114
|
+
pattern: 'fan-out',
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
// ── Coordinator ─────────────────────────────────────────────────────────────
|
|
118
|
+
export class SwarmCoordinator extends EventEmitter {
|
|
119
|
+
db;
|
|
120
|
+
constructor(db) {
|
|
121
|
+
super();
|
|
122
|
+
this.db = db;
|
|
123
|
+
}
|
|
124
|
+
// ── Pattern selection ───────────────────────────────────────────────────
|
|
125
|
+
/**
|
|
126
|
+
* Select the swarm pattern to use for a config. If the config already
|
|
127
|
+
* specifies a pattern, it is returned as-is. Otherwise heuristics apply.
|
|
128
|
+
*/
|
|
129
|
+
selectPattern(config) {
|
|
130
|
+
if (config.swarm.pattern) {
|
|
131
|
+
return config.swarm.pattern;
|
|
132
|
+
}
|
|
133
|
+
for (const h of PATTERN_HEURISTICS) {
|
|
134
|
+
if (h.test(config))
|
|
135
|
+
return h.pattern;
|
|
136
|
+
}
|
|
137
|
+
return 'fan-out';
|
|
138
|
+
}
|
|
139
|
+
// ── Topology resolution ─────────────────────────────────────────────────
|
|
140
|
+
/**
|
|
141
|
+
* Build the agent communication topology for a given config and pattern.
|
|
142
|
+
* Non-interactive agents are excluded from message edges — they only communicate
|
|
143
|
+
* through step output chaining ({{steps.X.output}}).
|
|
144
|
+
*/
|
|
145
|
+
resolveTopology(config, pattern) {
|
|
146
|
+
const p = pattern ?? this.selectPattern(config);
|
|
147
|
+
const agents = config.agents;
|
|
148
|
+
const edges = new Map();
|
|
149
|
+
// Non-interactive agents have no inbound or outbound message edges
|
|
150
|
+
const nonInteractiveNames = new Set(agents.filter((a) => a.interactive === false).map((a) => a.name));
|
|
151
|
+
const names = agents.map((a) => a.name).filter((n) => !nonInteractiveNames.has(n));
|
|
152
|
+
const topology = this.resolveInteractiveTopology(p, config, agents, edges, names);
|
|
153
|
+
// Apply non-interactive filtering to the actual topology edges (not the local
|
|
154
|
+
// `edges` variable, which may not be the same map — e.g., DAG creates its own).
|
|
155
|
+
const topologyEdges = topology.edges;
|
|
156
|
+
// Ensure non-interactive agents have empty edge entries (no messaging)
|
|
157
|
+
for (const name of nonInteractiveNames) {
|
|
158
|
+
topologyEdges.set(name, []);
|
|
159
|
+
}
|
|
160
|
+
// Also filter out non-interactive agents from any edge targets
|
|
161
|
+
for (const [agent, targets] of topologyEdges) {
|
|
162
|
+
topologyEdges.set(agent, targets.filter((t) => !nonInteractiveNames.has(t)));
|
|
163
|
+
}
|
|
164
|
+
return topology;
|
|
165
|
+
}
|
|
166
|
+
/** Internal: resolve topology edges for interactive agents only. */
|
|
167
|
+
resolveInteractiveTopology(p, config, agents, edges, names) {
|
|
168
|
+
switch (p) {
|
|
169
|
+
case 'fan-out': {
|
|
170
|
+
// Hub (first agent or role=lead) fans out to all others; no inter-worker edges.
|
|
171
|
+
const hub = this.pickHub(agents);
|
|
172
|
+
const others = names.filter((n) => n !== hub);
|
|
173
|
+
edges.set(hub, others);
|
|
174
|
+
for (const o of others)
|
|
175
|
+
edges.set(o, [hub]);
|
|
176
|
+
return { pattern: p, agents, edges, hub };
|
|
177
|
+
}
|
|
178
|
+
case 'pipeline': {
|
|
179
|
+
// Linear chain following workflow step order or agent list order.
|
|
180
|
+
const order = this.resolvePipelineOrder(config, names);
|
|
181
|
+
for (let i = 0; i < order.length; i++) {
|
|
182
|
+
edges.set(order[i], i < order.length - 1 ? [order[i + 1]] : []);
|
|
183
|
+
}
|
|
184
|
+
return { pattern: p, agents, edges, pipelineOrder: order };
|
|
185
|
+
}
|
|
186
|
+
case 'hub-spoke': {
|
|
187
|
+
const hub = this.pickHub(agents);
|
|
188
|
+
const spokes = names.filter((n) => n !== hub);
|
|
189
|
+
edges.set(hub, spokes);
|
|
190
|
+
for (const s of spokes)
|
|
191
|
+
edges.set(s, [hub]);
|
|
192
|
+
return { pattern: p, agents, edges, hub };
|
|
193
|
+
}
|
|
194
|
+
case 'consensus':
|
|
195
|
+
case 'debate':
|
|
196
|
+
case 'mesh': {
|
|
197
|
+
// Full mesh — every agent can talk to every other.
|
|
198
|
+
for (const n of names) {
|
|
199
|
+
edges.set(n, names.filter((o) => o !== n));
|
|
200
|
+
}
|
|
201
|
+
return { pattern: p, agents, edges };
|
|
202
|
+
}
|
|
203
|
+
case 'handoff': {
|
|
204
|
+
// Chain with explicit handoff: each agent passes to the next.
|
|
205
|
+
const order = this.resolvePipelineOrder(config, names);
|
|
206
|
+
for (let i = 0; i < order.length; i++) {
|
|
207
|
+
edges.set(order[i], i < order.length - 1 ? [order[i + 1]] : []);
|
|
208
|
+
}
|
|
209
|
+
return { pattern: p, agents, edges, pipelineOrder: order };
|
|
210
|
+
}
|
|
211
|
+
case 'cascade': {
|
|
212
|
+
// Primary tries first; on failure, falls through to next.
|
|
213
|
+
for (let i = 0; i < names.length; i++) {
|
|
214
|
+
edges.set(names[i], i < names.length - 1 ? [names[i + 1]] : []);
|
|
215
|
+
}
|
|
216
|
+
return { pattern: p, agents, edges, pipelineOrder: names };
|
|
217
|
+
}
|
|
218
|
+
case 'dag': {
|
|
219
|
+
// Edges derived from workflow step dependencies.
|
|
220
|
+
const stepEdges = this.resolveDAGEdges(config);
|
|
221
|
+
for (const n of names) {
|
|
222
|
+
if (!stepEdges.has(n))
|
|
223
|
+
stepEdges.set(n, []);
|
|
224
|
+
}
|
|
225
|
+
return { pattern: p, agents, edges: stepEdges };
|
|
226
|
+
}
|
|
227
|
+
case 'hierarchical': {
|
|
228
|
+
const hub = this.pickHub(agents);
|
|
229
|
+
const subordinates = names.filter((n) => n !== hub);
|
|
230
|
+
edges.set(hub, subordinates);
|
|
231
|
+
for (const s of subordinates)
|
|
232
|
+
edges.set(s, [hub]);
|
|
233
|
+
return { pattern: p, agents, edges, hub };
|
|
234
|
+
}
|
|
235
|
+
// ── Additional patterns ────────────────────────────────────────────
|
|
236
|
+
case 'map-reduce': {
|
|
237
|
+
// Mappers fan out from coordinator, all feed into reducer(s)
|
|
238
|
+
const coordinator = this.pickHub(agents);
|
|
239
|
+
const mappers = agents.filter((a) => a.role === 'mapper').map((a) => a.name);
|
|
240
|
+
const reducers = agents.filter((a) => a.role === 'reducer').map((a) => a.name);
|
|
241
|
+
const others = names.filter((n) => n !== coordinator && !mappers.includes(n) && !reducers.includes(n));
|
|
242
|
+
// Coordinator → mappers (excluding self if coordinator is also a mapper)
|
|
243
|
+
edges.set(coordinator, [...mappers.filter((m) => m !== coordinator), ...others]);
|
|
244
|
+
// Mappers → reducers (skip coordinator to avoid overwriting its edges)
|
|
245
|
+
for (const m of mappers) {
|
|
246
|
+
if (m === coordinator)
|
|
247
|
+
continue;
|
|
248
|
+
edges.set(m, reducers.length > 0 ? reducers : [coordinator]);
|
|
249
|
+
}
|
|
250
|
+
// Reducers → coordinator
|
|
251
|
+
for (const r of reducers)
|
|
252
|
+
edges.set(r, [coordinator]);
|
|
253
|
+
// Others → coordinator
|
|
254
|
+
for (const o of others)
|
|
255
|
+
edges.set(o, [coordinator]);
|
|
256
|
+
return { pattern: p, agents, edges, hub: coordinator };
|
|
257
|
+
}
|
|
258
|
+
case 'scatter-gather': {
|
|
259
|
+
// Hub scatters to all workers, gathers responses back
|
|
260
|
+
const hub = this.pickHub(agents);
|
|
261
|
+
const workers = names.filter((n) => n !== hub);
|
|
262
|
+
edges.set(hub, workers);
|
|
263
|
+
for (const w of workers)
|
|
264
|
+
edges.set(w, [hub]);
|
|
265
|
+
return { pattern: p, agents, edges, hub };
|
|
266
|
+
}
|
|
267
|
+
case 'supervisor': {
|
|
268
|
+
// Supervisor monitors all workers; workers report to supervisor
|
|
269
|
+
const supervisor = agents.find((a) => a.role === 'supervisor')?.name ?? this.pickHub(agents);
|
|
270
|
+
const workers = names.filter((n) => n !== supervisor);
|
|
271
|
+
edges.set(supervisor, workers);
|
|
272
|
+
for (const w of workers)
|
|
273
|
+
edges.set(w, [supervisor]);
|
|
274
|
+
return { pattern: p, agents, edges, hub: supervisor };
|
|
275
|
+
}
|
|
276
|
+
case 'reflection': {
|
|
277
|
+
// Agent produces output, critic reviews and sends feedback
|
|
278
|
+
// Linear: producer → critic → producer (loop-capable)
|
|
279
|
+
const critic = agents.find((a) => a.role === 'critic' || a.role === 'reviewer')?.name;
|
|
280
|
+
const producers = names.filter((n) => n !== critic);
|
|
281
|
+
if (critic) {
|
|
282
|
+
for (const prod of producers) {
|
|
283
|
+
edges.set(prod, [critic]);
|
|
284
|
+
}
|
|
285
|
+
edges.set(critic, producers);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
// Fallback: self-reflection via mesh
|
|
289
|
+
for (const n of names)
|
|
290
|
+
edges.set(n, names.filter((o) => o !== n));
|
|
291
|
+
}
|
|
292
|
+
return { pattern: p, agents, edges };
|
|
293
|
+
}
|
|
294
|
+
case 'red-team': {
|
|
295
|
+
// Attacker ↔ Defender adversarial communication
|
|
296
|
+
const attackers = agents.filter((a) => a.role === 'attacker' || a.role === 'red-team').map((a) => a.name);
|
|
297
|
+
const defenders = agents.filter((a) => a.role === 'defender' || a.role === 'blue-team').map((a) => a.name);
|
|
298
|
+
const judges = names.filter((n) => !attackers.includes(n) && !defenders.includes(n));
|
|
299
|
+
// Attackers → defenders and judges
|
|
300
|
+
for (const a of attackers)
|
|
301
|
+
edges.set(a, [...defenders, ...judges]);
|
|
302
|
+
// Defenders → attackers and judges
|
|
303
|
+
for (const d of defenders)
|
|
304
|
+
edges.set(d, [...attackers, ...judges]);
|
|
305
|
+
// Judges receive from both, can communicate with all
|
|
306
|
+
for (const j of judges)
|
|
307
|
+
edges.set(j, [...attackers, ...defenders]);
|
|
308
|
+
return { pattern: p, agents, edges };
|
|
309
|
+
}
|
|
310
|
+
case 'verifier': {
|
|
311
|
+
// Producer → Verifier chain; verifier can reject back to producer
|
|
312
|
+
const verifiers = agents.filter((a) => a.role === 'verifier').map((a) => a.name);
|
|
313
|
+
const producers = names.filter((n) => !verifiers.includes(n));
|
|
314
|
+
for (const prod of producers)
|
|
315
|
+
edges.set(prod, verifiers.length > 0 ? verifiers : []);
|
|
316
|
+
for (const v of verifiers)
|
|
317
|
+
edges.set(v, producers); // Can send rejections back
|
|
318
|
+
return { pattern: p, agents, edges };
|
|
319
|
+
}
|
|
320
|
+
case 'auction': {
|
|
321
|
+
// Auctioneer broadcasts tasks; bidders respond to auctioneer only
|
|
322
|
+
const auctioneer = agents.find((a) => a.role === 'auctioneer')?.name ?? this.pickHub(agents);
|
|
323
|
+
const bidders = names.filter((n) => n !== auctioneer);
|
|
324
|
+
edges.set(auctioneer, bidders);
|
|
325
|
+
for (const b of bidders)
|
|
326
|
+
edges.set(b, [auctioneer]);
|
|
327
|
+
return { pattern: p, agents, edges, hub: auctioneer };
|
|
328
|
+
}
|
|
329
|
+
case 'escalation': {
|
|
330
|
+
// Tiered chain: each level can escalate to the next
|
|
331
|
+
// Uses agent order or tier role numbers
|
|
332
|
+
const order = this.resolveEscalationOrder(agents);
|
|
333
|
+
for (let i = 0; i < order.length; i++) {
|
|
334
|
+
// Each tier can escalate up and report down
|
|
335
|
+
const canEscalateTo = i < order.length - 1 ? [order[i + 1]] : [];
|
|
336
|
+
const canReportTo = i > 0 ? [order[i - 1]] : [];
|
|
337
|
+
edges.set(order[i], [...canEscalateTo, ...canReportTo]);
|
|
338
|
+
}
|
|
339
|
+
// Ensure non-tiered agents still have edge entries (prevents undefined)
|
|
340
|
+
for (const n of names) {
|
|
341
|
+
if (!edges.has(n))
|
|
342
|
+
edges.set(n, []);
|
|
343
|
+
}
|
|
344
|
+
return { pattern: p, agents, edges, pipelineOrder: order };
|
|
345
|
+
}
|
|
346
|
+
case 'saga': {
|
|
347
|
+
// Orchestrator coordinates saga steps; each step can trigger compensate
|
|
348
|
+
const orchestrator = agents.find((a) => a.role === 'saga-orchestrator')?.name ?? this.pickHub(agents);
|
|
349
|
+
const participants = names.filter((n) => n !== orchestrator);
|
|
350
|
+
// Orchestrator → all participants (for commands)
|
|
351
|
+
edges.set(orchestrator, participants);
|
|
352
|
+
// Participants → orchestrator (for completion/failure signals)
|
|
353
|
+
for (const part of participants)
|
|
354
|
+
edges.set(part, [orchestrator]);
|
|
355
|
+
return { pattern: p, agents, edges, hub: orchestrator };
|
|
356
|
+
}
|
|
357
|
+
case 'circuit-breaker': {
|
|
358
|
+
// Primary agent with fallback chain
|
|
359
|
+
const order = names; // First agent is primary, rest are fallbacks
|
|
360
|
+
for (let i = 0; i < order.length; i++) {
|
|
361
|
+
// Each can trigger next fallback
|
|
362
|
+
edges.set(order[i], i < order.length - 1 ? [order[i + 1]] : []);
|
|
363
|
+
}
|
|
364
|
+
return { pattern: p, agents, edges, pipelineOrder: order };
|
|
365
|
+
}
|
|
366
|
+
case 'blackboard': {
|
|
367
|
+
// All agents can read/write to shared blackboard (full mesh)
|
|
368
|
+
// Plus optional moderator
|
|
369
|
+
const moderator = agents.find((a) => a.role === 'moderator')?.name;
|
|
370
|
+
for (const n of names) {
|
|
371
|
+
edges.set(n, names.filter((o) => o !== n));
|
|
372
|
+
}
|
|
373
|
+
return { pattern: p, agents, edges, hub: moderator };
|
|
374
|
+
}
|
|
375
|
+
case 'swarm': {
|
|
376
|
+
// Emergent swarm: agents communicate with nearest neighbors
|
|
377
|
+
// For simplicity, partial mesh based on agent index proximity
|
|
378
|
+
const hiveMind = agents.find((a) => a.role === 'hive-mind')?.name;
|
|
379
|
+
for (let i = 0; i < names.length; i++) {
|
|
380
|
+
const neighbors = [];
|
|
381
|
+
if (i > 0)
|
|
382
|
+
neighbors.push(names[i - 1]);
|
|
383
|
+
if (i < names.length - 1)
|
|
384
|
+
neighbors.push(names[i + 1]);
|
|
385
|
+
// Also connect to hive mind if present (avoid duplicates if already adjacent)
|
|
386
|
+
if (hiveMind && hiveMind !== names[i] && !neighbors.includes(hiveMind))
|
|
387
|
+
neighbors.push(hiveMind);
|
|
388
|
+
edges.set(names[i], neighbors);
|
|
389
|
+
}
|
|
390
|
+
return { pattern: p, agents, edges, hub: hiveMind };
|
|
391
|
+
}
|
|
392
|
+
case 'review-loop': {
|
|
393
|
+
// Implementer is hub; reviewers can communicate with implementer AND each other
|
|
394
|
+
// This enables collaborative review where reviewers can discuss findings
|
|
395
|
+
const implementer = agents.find((a) => a.role?.toLowerCase().includes('implement') || a.name.toLowerCase().includes('implement'))?.name ?? this.pickHub(agents);
|
|
396
|
+
const reviewers = agents
|
|
397
|
+
.filter((a) => a.name !== implementer &&
|
|
398
|
+
(a.role?.toLowerCase().includes('reviewer') || a.name.toLowerCase().includes('reviewer')))
|
|
399
|
+
.map((a) => a.name);
|
|
400
|
+
const others = names.filter((n) => n !== implementer && !reviewers.includes(n));
|
|
401
|
+
// Implementer → all reviewers and others
|
|
402
|
+
edges.set(implementer, [...reviewers, ...others]);
|
|
403
|
+
// Reviewers → implementer + other reviewers (collaborative review)
|
|
404
|
+
for (const r of reviewers) {
|
|
405
|
+
const otherReviewers = reviewers.filter((or) => or !== r);
|
|
406
|
+
edges.set(r, [implementer, ...otherReviewers]);
|
|
407
|
+
}
|
|
408
|
+
// Others → implementer
|
|
409
|
+
for (const o of others)
|
|
410
|
+
edges.set(o, [implementer]);
|
|
411
|
+
return { pattern: p, agents, edges, hub: implementer };
|
|
412
|
+
}
|
|
413
|
+
default: {
|
|
414
|
+
// Fallback: full mesh.
|
|
415
|
+
for (const n of names) {
|
|
416
|
+
edges.set(n, names.filter((o) => o !== n));
|
|
417
|
+
}
|
|
418
|
+
return { pattern: p, agents, edges };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// ── Lifecycle: create run ───────────────────────────────────────────────
|
|
423
|
+
async createRun(workspaceId, config) {
|
|
424
|
+
const id = `run_${Date.now()}_${randomBytes(4).toString('hex')}`;
|
|
425
|
+
const pattern = this.selectPattern(config);
|
|
426
|
+
const now = new Date().toISOString();
|
|
427
|
+
const { rows } = await this.db.query(`INSERT INTO workflow_runs (id, workspace_id, workflow_name, pattern, status, config, started_at, created_at, updated_at)
|
|
428
|
+
VALUES ($1, $2, $3, $4, 'pending', $5, $6, $6, $6)
|
|
429
|
+
RETURNING *`, [id, workspaceId, config.name, pattern, JSON.stringify(config), now]);
|
|
430
|
+
const run = rows[0];
|
|
431
|
+
this.emit('run:created', run);
|
|
432
|
+
return run;
|
|
433
|
+
}
|
|
434
|
+
// ── Lifecycle: start run ────────────────────────────────────────────────
|
|
435
|
+
async startRun(runId) {
|
|
436
|
+
const now = new Date().toISOString();
|
|
437
|
+
const { rows } = await this.db.query(`UPDATE workflow_runs SET status = 'running', started_at = $2, updated_at = $2
|
|
438
|
+
WHERE id = $1 AND status = 'pending'
|
|
439
|
+
RETURNING *`, [runId, now]);
|
|
440
|
+
if (rows.length === 0) {
|
|
441
|
+
throw new Error(`Run ${runId} not found or not in pending state`);
|
|
442
|
+
}
|
|
443
|
+
const run = rows[0];
|
|
444
|
+
this.emit('run:started', run);
|
|
445
|
+
return run;
|
|
446
|
+
}
|
|
447
|
+
// ── Lifecycle: complete / fail / cancel ─────────────────────────────────
|
|
448
|
+
async completeRun(runId, stateSnapshot) {
|
|
449
|
+
return this.transitionRun(runId, 'completed', undefined, stateSnapshot);
|
|
450
|
+
}
|
|
451
|
+
async failRun(runId, error) {
|
|
452
|
+
return this.transitionRun(runId, 'failed', error);
|
|
453
|
+
}
|
|
454
|
+
async cancelRun(runId) {
|
|
455
|
+
return this.transitionRun(runId, 'cancelled');
|
|
456
|
+
}
|
|
457
|
+
// ── Step management ─────────────────────────────────────────────────────
|
|
458
|
+
async createSteps(runId, config) {
|
|
459
|
+
const workflows = config.workflows ?? [];
|
|
460
|
+
const created = [];
|
|
461
|
+
for (const wf of workflows) {
|
|
462
|
+
for (const step of wf.steps) {
|
|
463
|
+
const id = `step_${Date.now()}_${randomBytes(4).toString('hex')}`;
|
|
464
|
+
const now = new Date().toISOString();
|
|
465
|
+
const { rows } = await this.db.query(`INSERT INTO workflow_steps (id, run_id, step_name, agent_name, status, task, depends_on, created_at, updated_at)
|
|
466
|
+
VALUES ($1, $2, $3, $4, 'pending', $5, $6, $7, $7)
|
|
467
|
+
RETURNING *`, [
|
|
468
|
+
id,
|
|
469
|
+
runId,
|
|
470
|
+
step.name,
|
|
471
|
+
step.agent ?? null,
|
|
472
|
+
step.task ?? step.command ?? '',
|
|
473
|
+
JSON.stringify(step.dependsOn ?? []),
|
|
474
|
+
now,
|
|
475
|
+
]);
|
|
476
|
+
created.push(rows[0]);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return created;
|
|
480
|
+
}
|
|
481
|
+
async startStep(stepId) {
|
|
482
|
+
const now = new Date().toISOString();
|
|
483
|
+
const { rows } = await this.db.query(`UPDATE workflow_steps SET status = 'running', started_at = $2, updated_at = $2
|
|
484
|
+
WHERE id = $1 AND status = 'pending'
|
|
485
|
+
RETURNING *`, [stepId, now]);
|
|
486
|
+
if (rows.length === 0) {
|
|
487
|
+
throw new Error(`Step ${stepId} not found or not in pending state`);
|
|
488
|
+
}
|
|
489
|
+
const step = rows[0];
|
|
490
|
+
this.emit('step:started', step);
|
|
491
|
+
return step;
|
|
492
|
+
}
|
|
493
|
+
async completeStep(stepId, output) {
|
|
494
|
+
const now = new Date().toISOString();
|
|
495
|
+
const { rows } = await this.db.query(`UPDATE workflow_steps SET status = 'completed', output = $2, completed_at = $3, updated_at = $3
|
|
496
|
+
WHERE id = $1 AND status = 'running'
|
|
497
|
+
RETURNING *`, [stepId, output ?? null, now]);
|
|
498
|
+
if (rows.length === 0) {
|
|
499
|
+
throw new Error(`Step ${stepId} not found or not in running state`);
|
|
500
|
+
}
|
|
501
|
+
const step = rows[0];
|
|
502
|
+
this.emit('step:completed', step);
|
|
503
|
+
return step;
|
|
504
|
+
}
|
|
505
|
+
async failStep(stepId, error) {
|
|
506
|
+
const now = new Date().toISOString();
|
|
507
|
+
const { rows } = await this.db.query(`UPDATE workflow_steps SET status = 'failed', error = $2, completed_at = $3, updated_at = $3
|
|
508
|
+
WHERE id = $1 AND status = 'running'
|
|
509
|
+
RETURNING *`, [stepId, error, now]);
|
|
510
|
+
if (rows.length === 0) {
|
|
511
|
+
throw new Error(`Step ${stepId} not found or not in running state`);
|
|
512
|
+
}
|
|
513
|
+
const step = rows[0];
|
|
514
|
+
this.emit('step:failed', step);
|
|
515
|
+
return step;
|
|
516
|
+
}
|
|
517
|
+
async skipStep(stepId) {
|
|
518
|
+
const now = new Date().toISOString();
|
|
519
|
+
const { rows } = await this.db.query(`UPDATE workflow_steps SET status = 'skipped', completed_at = $2, updated_at = $2
|
|
520
|
+
WHERE id = $1
|
|
521
|
+
RETURNING *`, [stepId, now]);
|
|
522
|
+
if (rows.length === 0) {
|
|
523
|
+
throw new Error(`Step ${stepId} not found`);
|
|
524
|
+
}
|
|
525
|
+
return rows[0];
|
|
526
|
+
}
|
|
527
|
+
// ── Queries ─────────────────────────────────────────────────────────────
|
|
528
|
+
async getRun(runId) {
|
|
529
|
+
const { rows } = await this.db.query(`SELECT * FROM workflow_runs WHERE id = $1`, [runId]);
|
|
530
|
+
return rows[0] ?? null;
|
|
531
|
+
}
|
|
532
|
+
async getSteps(runId) {
|
|
533
|
+
const { rows } = await this.db.query(`SELECT * FROM workflow_steps WHERE run_id = $1 ORDER BY created_at ASC`, [runId]);
|
|
534
|
+
return rows;
|
|
535
|
+
}
|
|
536
|
+
async getReadySteps(runId) {
|
|
537
|
+
const steps = await this.getSteps(runId);
|
|
538
|
+
const completedNames = new Set(steps.filter((s) => s.status === 'completed').map((s) => s.stepName));
|
|
539
|
+
return steps.filter((s) => {
|
|
540
|
+
if (s.status !== 'pending')
|
|
541
|
+
return false;
|
|
542
|
+
const deps = Array.isArray(s.dependsOn) ? s.dependsOn : [];
|
|
543
|
+
return deps.every((d) => completedNames.has(d));
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async getRunsByWorkspace(workspaceId, status) {
|
|
547
|
+
if (status) {
|
|
548
|
+
const { rows } = await this.db.query(`SELECT * FROM workflow_runs WHERE workspace_id = $1 AND status = $2 ORDER BY created_at DESC`, [workspaceId, status]);
|
|
549
|
+
return rows;
|
|
550
|
+
}
|
|
551
|
+
const { rows } = await this.db.query(`SELECT * FROM workflow_runs WHERE workspace_id = $1 ORDER BY created_at DESC`, [workspaceId]);
|
|
552
|
+
return rows;
|
|
553
|
+
}
|
|
554
|
+
// ── Private helpers ─────────────────────────────────────────────────────
|
|
555
|
+
async transitionRun(runId, status, error, stateSnapshot) {
|
|
556
|
+
const now = new Date().toISOString();
|
|
557
|
+
const { rows } = await this.db.query(`UPDATE workflow_runs
|
|
558
|
+
SET status = $2, completed_at = $3, error = $4, state_snapshot = $5, updated_at = $3
|
|
559
|
+
WHERE id = $1
|
|
560
|
+
RETURNING *`, [
|
|
561
|
+
runId,
|
|
562
|
+
status,
|
|
563
|
+
now,
|
|
564
|
+
error ?? null,
|
|
565
|
+
stateSnapshot ? JSON.stringify(stateSnapshot) : null,
|
|
566
|
+
]);
|
|
567
|
+
if (rows.length === 0) {
|
|
568
|
+
throw new Error(`Run ${runId} not found`);
|
|
569
|
+
}
|
|
570
|
+
const run = rows[0];
|
|
571
|
+
const eventName = `run:${status}`;
|
|
572
|
+
this.emit(eventName, run);
|
|
573
|
+
return run;
|
|
574
|
+
}
|
|
575
|
+
pickHub(agents) {
|
|
576
|
+
// Prefer interactive agents as hub — non-interactive agents cannot receive messages
|
|
577
|
+
const interactiveAgents = agents.filter((a) => a.interactive !== false);
|
|
578
|
+
const pool = interactiveAgents.length > 0 ? interactiveAgents : agents;
|
|
579
|
+
const lead = pool.find((a) => a.role === 'lead' || a.role === 'hub' || a.role === 'coordinator');
|
|
580
|
+
return lead?.name ?? pool[0].name;
|
|
581
|
+
}
|
|
582
|
+
resolvePipelineOrder(config, fallback) {
|
|
583
|
+
const workflow = config.workflows?.[0];
|
|
584
|
+
if (!workflow)
|
|
585
|
+
return fallback;
|
|
586
|
+
// Use step order — each step's agent in sequence, deduped.
|
|
587
|
+
const seen = new Set();
|
|
588
|
+
const order = [];
|
|
589
|
+
for (const step of workflow.steps) {
|
|
590
|
+
// Skip deterministic steps (no agent)
|
|
591
|
+
if (!step.agent)
|
|
592
|
+
continue;
|
|
593
|
+
if (!seen.has(step.agent)) {
|
|
594
|
+
seen.add(step.agent);
|
|
595
|
+
order.push(step.agent);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return order.length > 0 ? order : fallback;
|
|
599
|
+
}
|
|
600
|
+
resolveEscalationOrder(agents) {
|
|
601
|
+
// Sort by tier role (e.g., "tier-1", "tier-2") or by agent order
|
|
602
|
+
const tiered = agents.filter((a) => a.role?.startsWith('tier-'));
|
|
603
|
+
if (tiered.length > 0) {
|
|
604
|
+
return tiered
|
|
605
|
+
.sort((a, b) => {
|
|
606
|
+
const tierA = parseInt(a.role?.replace('tier-', '') ?? '0', 10);
|
|
607
|
+
const tierB = parseInt(b.role?.replace('tier-', '') ?? '0', 10);
|
|
608
|
+
return tierA - tierB;
|
|
609
|
+
})
|
|
610
|
+
.map((a) => a.name);
|
|
611
|
+
}
|
|
612
|
+
// Fallback: use agent order
|
|
613
|
+
return agents.map((a) => a.name);
|
|
614
|
+
}
|
|
615
|
+
resolveDAGEdges(config) {
|
|
616
|
+
const edges = new Map();
|
|
617
|
+
const workflows = config.workflows ?? [];
|
|
618
|
+
for (const wf of workflows) {
|
|
619
|
+
// Build step-name → agent-name mapping (skip deterministic steps)
|
|
620
|
+
const stepAgent = new Map();
|
|
621
|
+
for (const step of wf.steps) {
|
|
622
|
+
if (step.agent) {
|
|
623
|
+
stepAgent.set(step.name, step.agent);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
for (const step of wf.steps) {
|
|
627
|
+
// Skip deterministic steps
|
|
628
|
+
if (!step.agent)
|
|
629
|
+
continue;
|
|
630
|
+
if (!step.dependsOn?.length)
|
|
631
|
+
continue;
|
|
632
|
+
for (const dep of step.dependsOn) {
|
|
633
|
+
const fromAgent = stepAgent.get(dep);
|
|
634
|
+
if (!fromAgent)
|
|
635
|
+
continue;
|
|
636
|
+
const existing = edges.get(fromAgent) ?? [];
|
|
637
|
+
if (!existing.includes(step.agent)) {
|
|
638
|
+
existing.push(step.agent);
|
|
639
|
+
}
|
|
640
|
+
edges.set(fromAgent, existing);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return edges;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
//# sourceMappingURL=coordinator.js.map
|