agent-relay 2.3.11 → 2.3.13
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/install.sh +32 -0
- package/package.json +21 -21
- package/packages/acp-bridge/package.json +2 -2
- package/packages/bridge/package.json +7 -7
- package/packages/broker-sdk/README.md +32 -0
- package/packages/broker-sdk/dist/__tests__/unit.test.js +70 -2
- package/packages/broker-sdk/dist/__tests__/unit.test.js.map +1 -1
- package/packages/broker-sdk/dist/client.d.ts +2 -0
- package/packages/broker-sdk/dist/client.d.ts.map +1 -1
- package/packages/broker-sdk/dist/client.js +10 -0
- package/packages/broker-sdk/dist/client.js.map +1 -1
- package/packages/broker-sdk/dist/protocol.d.ts +4 -0
- package/packages/broker-sdk/dist/protocol.d.ts.map +1 -1
- package/packages/broker-sdk/dist/relay.d.ts +10 -0
- package/packages/broker-sdk/dist/relay.d.ts.map +1 -1
- package/packages/broker-sdk/dist/relay.js +53 -0
- package/packages/broker-sdk/dist/relay.js.map +1 -1
- package/packages/broker-sdk/dist/relaycast.d.ts +10 -0
- package/packages/broker-sdk/dist/relaycast.d.ts.map +1 -1
- package/packages/broker-sdk/dist/relaycast.js +40 -0
- package/packages/broker-sdk/dist/relaycast.js.map +1 -1
- package/packages/broker-sdk/dist/workflows/coordinator.d.ts +1 -0
- package/packages/broker-sdk/dist/workflows/coordinator.d.ts.map +1 -1
- package/packages/broker-sdk/dist/workflows/coordinator.js +239 -7
- package/packages/broker-sdk/dist/workflows/coordinator.js.map +1 -1
- package/packages/broker-sdk/dist/workflows/index.d.ts +1 -0
- package/packages/broker-sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/broker-sdk/dist/workflows/index.js +1 -0
- package/packages/broker-sdk/dist/workflows/index.js.map +1 -1
- package/packages/broker-sdk/dist/workflows/run.d.ts +3 -1
- package/packages/broker-sdk/dist/workflows/run.d.ts.map +1 -1
- package/packages/broker-sdk/dist/workflows/run.js +4 -0
- package/packages/broker-sdk/dist/workflows/run.js.map +1 -1
- package/packages/broker-sdk/dist/workflows/runner.d.ts +9 -0
- package/packages/broker-sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/broker-sdk/dist/workflows/runner.js +203 -14
- package/packages/broker-sdk/dist/workflows/runner.js.map +1 -1
- package/packages/broker-sdk/dist/workflows/trajectory.d.ts +80 -0
- package/packages/broker-sdk/dist/workflows/trajectory.d.ts.map +1 -0
- package/packages/broker-sdk/dist/workflows/trajectory.js +362 -0
- package/packages/broker-sdk/dist/workflows/trajectory.js.map +1 -0
- package/packages/broker-sdk/dist/workflows/types.d.ts +15 -1
- package/packages/broker-sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/broker-sdk/package.json +2 -2
- package/packages/broker-sdk/src/__tests__/swarm-coordinator.test.ts +356 -0
- package/packages/broker-sdk/src/__tests__/unit.test.ts +92 -1
- package/packages/broker-sdk/src/__tests__/workflow-trajectory.test.ts +408 -0
- package/packages/broker-sdk/src/client.ts +15 -0
- package/packages/broker-sdk/src/protocol.ts +5 -0
- package/packages/broker-sdk/src/relay.ts +59 -0
- package/packages/broker-sdk/src/relaycast.ts +42 -0
- package/packages/broker-sdk/src/workflows/README.md +64 -0
- package/packages/broker-sdk/src/workflows/coordinator.ts +246 -8
- package/packages/broker-sdk/src/workflows/index.ts +1 -0
- package/packages/broker-sdk/src/workflows/run.ts +9 -1
- package/packages/broker-sdk/src/workflows/runner.ts +249 -14
- package/packages/broker-sdk/src/workflows/schema.json +13 -1
- package/packages/broker-sdk/src/workflows/trajectory.ts +507 -0
- package/packages/broker-sdk/src/workflows/types.ts +31 -1
- package/packages/broker-sdk/tsconfig.json +1 -0
- package/packages/broker-sdk/vitest.config.ts +9 -0
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +2 -2
- package/packages/daemon/package.json +12 -12
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/package.json +5 -5
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/package.json +3 -3
- package/packages/sdk-py/src/agent_relay/builder.py +4 -0
- package/packages/sdk-py/src/agent_relay/types.py +15 -0
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +3 -3
- package/packages/wrapper/package.json +5 -5
|
@@ -15,6 +15,18 @@ export interface RelayYamlConfig {
|
|
|
15
15
|
coordination?: CoordinationConfig;
|
|
16
16
|
state?: StateConfig;
|
|
17
17
|
errorHandling?: ErrorHandlingConfig;
|
|
18
|
+
trajectories?: TrajectoryConfig | false;
|
|
19
|
+
}
|
|
20
|
+
/** Configuration for workflow trajectory recording. */
|
|
21
|
+
export interface TrajectoryConfig {
|
|
22
|
+
/** Enable trajectory recording (default: true). */
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/** Auto-reflect when barriers resolve (default: true). */
|
|
25
|
+
reflectOnBarriers?: boolean;
|
|
26
|
+
/** Auto-reflect when parallel tracks converge (default: true). */
|
|
27
|
+
reflectOnConverge?: boolean;
|
|
28
|
+
/** Record retry/skip/fail decisions automatically (default: true). */
|
|
29
|
+
autoDecisions?: boolean;
|
|
18
30
|
}
|
|
19
31
|
/** Swarm-level settings controlling the overall pattern. */
|
|
20
32
|
export interface SwarmConfig {
|
|
@@ -23,7 +35,7 @@ export interface SwarmConfig {
|
|
|
23
35
|
timeoutMs?: number;
|
|
24
36
|
channel?: string;
|
|
25
37
|
}
|
|
26
|
-
export type SwarmPattern = "fan-out" | "pipeline" | "hub-spoke" | "consensus" | "mesh" | "handoff" | "cascade" | "dag" | "debate" | "hierarchical";
|
|
38
|
+
export type SwarmPattern = "fan-out" | "pipeline" | "hub-spoke" | "consensus" | "mesh" | "handoff" | "cascade" | "dag" | "debate" | "hierarchical" | "map-reduce" | "scatter-gather" | "supervisor" | "reflection" | "red-team" | "verifier" | "auction" | "escalation" | "saga" | "circuit-breaker" | "blackboard" | "swarm";
|
|
27
39
|
/** Definition of an agent participating in a workflow. */
|
|
28
40
|
export interface AgentDefinition {
|
|
29
41
|
name: string;
|
|
@@ -40,6 +52,8 @@ export interface AgentConstraints {
|
|
|
40
52
|
timeoutMs?: number;
|
|
41
53
|
retries?: number;
|
|
42
54
|
model?: string;
|
|
55
|
+
/** Silence duration in seconds before the agent is considered idle (0 = disabled, default: 30). */
|
|
56
|
+
idleThresholdSecs?: number;
|
|
43
57
|
}
|
|
44
58
|
/** A named workflow composed of sequential or parallel steps. */
|
|
45
59
|
export interface WorkflowDefinition {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/workflows/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACjC,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,aAAa,CAAC,EAAE,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/workflows/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/broker-sdk",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"typescript": "^5.7.3"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@relaycast/sdk": "^0.
|
|
78
|
+
"@relaycast/sdk": "^0.3.0",
|
|
79
79
|
"yaml": "^2.7.0"
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -111,6 +111,107 @@ describe('SwarmCoordinator', () => {
|
|
|
111
111
|
});
|
|
112
112
|
expect(coordinator.selectPattern(config)).toBe('consensus');
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
// ── Auto-selection heuristic tests ──────────────────────────────────
|
|
116
|
+
|
|
117
|
+
it('should auto-select map-reduce when mapper and reducer roles present', () => {
|
|
118
|
+
const config = makeConfig({
|
|
119
|
+
swarm: { pattern: '' as any },
|
|
120
|
+
agents: [
|
|
121
|
+
{ name: 'mapper', cli: 'claude', role: 'mapper' },
|
|
122
|
+
{ name: 'reducer', cli: 'claude', role: 'reducer' },
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
expect(coordinator.selectPattern(config)).toBe('map-reduce');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should auto-select red-team when attacker and defender roles present', () => {
|
|
129
|
+
const config = makeConfig({
|
|
130
|
+
swarm: { pattern: '' as any },
|
|
131
|
+
agents: [
|
|
132
|
+
{ name: 'attacker', cli: 'claude', role: 'attacker' },
|
|
133
|
+
{ name: 'defender', cli: 'claude', role: 'defender' },
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
expect(coordinator.selectPattern(config)).toBe('red-team');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should auto-select reflection when critic role present', () => {
|
|
140
|
+
const config = makeConfig({
|
|
141
|
+
swarm: { pattern: '' as any },
|
|
142
|
+
agents: [
|
|
143
|
+
{ name: 'producer', cli: 'claude' },
|
|
144
|
+
{ name: 'critic', cli: 'claude', role: 'critic' },
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
expect(coordinator.selectPattern(config)).toBe('reflection');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should auto-select escalation when tier-N roles present', () => {
|
|
151
|
+
const config = makeConfig({
|
|
152
|
+
swarm: { pattern: '' as any },
|
|
153
|
+
agents: [
|
|
154
|
+
{ name: 't1', cli: 'claude', role: 'tier-1' },
|
|
155
|
+
{ name: 't2', cli: 'claude', role: 'tier-2' },
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
expect(coordinator.selectPattern(config)).toBe('escalation');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should auto-select auction when auctioneer role present', () => {
|
|
162
|
+
const config = makeConfig({
|
|
163
|
+
swarm: { pattern: '' as any },
|
|
164
|
+
agents: [
|
|
165
|
+
{ name: 'auctioneer', cli: 'claude', role: 'auctioneer' },
|
|
166
|
+
{ name: 'bidder', cli: 'claude' },
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
expect(coordinator.selectPattern(config)).toBe('auction');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should auto-select supervisor when supervisor role present', () => {
|
|
173
|
+
const config = makeConfig({
|
|
174
|
+
swarm: { pattern: '' as any },
|
|
175
|
+
agents: [
|
|
176
|
+
{ name: 'supervisor', cli: 'claude', role: 'supervisor' },
|
|
177
|
+
{ name: 'worker', cli: 'claude' },
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
expect(coordinator.selectPattern(config)).toBe('supervisor');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should auto-select verifier when verifier role present', () => {
|
|
184
|
+
const config = makeConfig({
|
|
185
|
+
swarm: { pattern: '' as any },
|
|
186
|
+
agents: [
|
|
187
|
+
{ name: 'producer', cli: 'claude' },
|
|
188
|
+
{ name: 'verifier', cli: 'claude', role: 'verifier' },
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
expect(coordinator.selectPattern(config)).toBe('verifier');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should auto-select swarm when hive-mind role present', () => {
|
|
195
|
+
const config = makeConfig({
|
|
196
|
+
swarm: { pattern: '' as any },
|
|
197
|
+
agents: [
|
|
198
|
+
{ name: 'hive', cli: 'claude', role: 'hive-mind' },
|
|
199
|
+
{ name: 'drone', cli: 'claude' },
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
expect(coordinator.selectPattern(config)).toBe('swarm');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should auto-select circuit-breaker when fallback role present', () => {
|
|
206
|
+
const config = makeConfig({
|
|
207
|
+
swarm: { pattern: '' as any },
|
|
208
|
+
agents: [
|
|
209
|
+
{ name: 'primary', cli: 'claude', role: 'primary' },
|
|
210
|
+
{ name: 'fallback', cli: 'claude', role: 'fallback' },
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
expect(coordinator.selectPattern(config)).toBe('circuit-breaker');
|
|
214
|
+
});
|
|
114
215
|
});
|
|
115
216
|
|
|
116
217
|
// ── Topology resolution ────────────────────────────────────────────────
|
|
@@ -191,6 +292,261 @@ describe('SwarmCoordinator', () => {
|
|
|
191
292
|
const topology = coordinator.resolveTopology(config);
|
|
192
293
|
expect(topology.pipelineOrder).toEqual(['leader', 'worker-1', 'worker-2']);
|
|
193
294
|
});
|
|
295
|
+
|
|
296
|
+
// ── Additional pattern tests ────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
it('should build map-reduce topology', () => {
|
|
299
|
+
const config = makeConfig({
|
|
300
|
+
swarm: { pattern: 'map-reduce' },
|
|
301
|
+
agents: [
|
|
302
|
+
{ name: 'coordinator', cli: 'claude', role: 'lead' },
|
|
303
|
+
{ name: 'mapper-1', cli: 'claude', role: 'mapper' },
|
|
304
|
+
{ name: 'mapper-2', cli: 'claude', role: 'mapper' },
|
|
305
|
+
{ name: 'reducer', cli: 'claude', role: 'reducer' },
|
|
306
|
+
],
|
|
307
|
+
});
|
|
308
|
+
const topology = coordinator.resolveTopology(config);
|
|
309
|
+
expect(topology.pattern).toBe('map-reduce');
|
|
310
|
+
expect(topology.hub).toBe('coordinator');
|
|
311
|
+
expect(topology.edges.get('coordinator')).toContain('mapper-1');
|
|
312
|
+
expect(topology.edges.get('mapper-1')).toContain('reducer');
|
|
313
|
+
expect(topology.edges.get('reducer')).toContain('coordinator');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should build scatter-gather topology', () => {
|
|
317
|
+
const config = makeConfig({ swarm: { pattern: 'scatter-gather' } });
|
|
318
|
+
const topology = coordinator.resolveTopology(config);
|
|
319
|
+
expect(topology.pattern).toBe('scatter-gather');
|
|
320
|
+
expect(topology.hub).toBe('leader');
|
|
321
|
+
expect(topology.edges.get('leader')).toContain('worker-1');
|
|
322
|
+
expect(topology.edges.get('worker-1')).toEqual(['leader']);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should build supervisor topology', () => {
|
|
326
|
+
const config = makeConfig({
|
|
327
|
+
swarm: { pattern: 'supervisor' },
|
|
328
|
+
agents: [
|
|
329
|
+
{ name: 'supervisor', cli: 'claude', role: 'supervisor' },
|
|
330
|
+
{ name: 'worker-1', cli: 'claude' },
|
|
331
|
+
{ name: 'worker-2', cli: 'codex' },
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
const topology = coordinator.resolveTopology(config);
|
|
335
|
+
expect(topology.pattern).toBe('supervisor');
|
|
336
|
+
expect(topology.hub).toBe('supervisor');
|
|
337
|
+
expect(topology.edges.get('supervisor')).toContain('worker-1');
|
|
338
|
+
expect(topology.edges.get('worker-1')).toEqual(['supervisor']);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should build reflection topology', () => {
|
|
342
|
+
const config = makeConfig({
|
|
343
|
+
swarm: { pattern: 'reflection' },
|
|
344
|
+
agents: [
|
|
345
|
+
{ name: 'producer', cli: 'claude' },
|
|
346
|
+
{ name: 'critic', cli: 'claude', role: 'critic' },
|
|
347
|
+
],
|
|
348
|
+
});
|
|
349
|
+
const topology = coordinator.resolveTopology(config);
|
|
350
|
+
expect(topology.pattern).toBe('reflection');
|
|
351
|
+
expect(topology.edges.get('producer')).toContain('critic');
|
|
352
|
+
expect(topology.edges.get('critic')).toContain('producer');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should build red-team topology', () => {
|
|
356
|
+
const config = makeConfig({
|
|
357
|
+
swarm: { pattern: 'red-team' },
|
|
358
|
+
agents: [
|
|
359
|
+
{ name: 'attacker', cli: 'claude', role: 'attacker' },
|
|
360
|
+
{ name: 'defender', cli: 'claude', role: 'defender' },
|
|
361
|
+
{ name: 'judge', cli: 'claude', role: 'judge' },
|
|
362
|
+
],
|
|
363
|
+
});
|
|
364
|
+
const topology = coordinator.resolveTopology(config);
|
|
365
|
+
expect(topology.pattern).toBe('red-team');
|
|
366
|
+
expect(topology.edges.get('attacker')).toContain('defender');
|
|
367
|
+
expect(topology.edges.get('defender')).toContain('attacker');
|
|
368
|
+
expect(topology.edges.get('attacker')).toContain('judge');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should build verifier topology', () => {
|
|
372
|
+
const config = makeConfig({
|
|
373
|
+
swarm: { pattern: 'verifier' },
|
|
374
|
+
agents: [
|
|
375
|
+
{ name: 'producer', cli: 'claude' },
|
|
376
|
+
{ name: 'verifier', cli: 'claude', role: 'verifier' },
|
|
377
|
+
],
|
|
378
|
+
});
|
|
379
|
+
const topology = coordinator.resolveTopology(config);
|
|
380
|
+
expect(topology.pattern).toBe('verifier');
|
|
381
|
+
expect(topology.edges.get('producer')).toContain('verifier');
|
|
382
|
+
expect(topology.edges.get('verifier')).toContain('producer');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should build auction topology', () => {
|
|
386
|
+
const config = makeConfig({
|
|
387
|
+
swarm: { pattern: 'auction' },
|
|
388
|
+
agents: [
|
|
389
|
+
{ name: 'auctioneer', cli: 'claude', role: 'auctioneer' },
|
|
390
|
+
{ name: 'bidder-1', cli: 'claude' },
|
|
391
|
+
{ name: 'bidder-2', cli: 'codex' },
|
|
392
|
+
],
|
|
393
|
+
});
|
|
394
|
+
const topology = coordinator.resolveTopology(config);
|
|
395
|
+
expect(topology.pattern).toBe('auction');
|
|
396
|
+
expect(topology.hub).toBe('auctioneer');
|
|
397
|
+
expect(topology.edges.get('auctioneer')).toContain('bidder-1');
|
|
398
|
+
expect(topology.edges.get('bidder-1')).toEqual(['auctioneer']);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should build escalation topology', () => {
|
|
402
|
+
const config = makeConfig({
|
|
403
|
+
swarm: { pattern: 'escalation' },
|
|
404
|
+
agents: [
|
|
405
|
+
{ name: 'tier1', cli: 'claude', role: 'tier-1' },
|
|
406
|
+
{ name: 'tier2', cli: 'claude', role: 'tier-2' },
|
|
407
|
+
{ name: 'tier3', cli: 'claude', role: 'tier-3' },
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
const topology = coordinator.resolveTopology(config);
|
|
411
|
+
expect(topology.pattern).toBe('escalation');
|
|
412
|
+
expect(topology.pipelineOrder).toEqual(['tier1', 'tier2', 'tier3']);
|
|
413
|
+
expect(topology.edges.get('tier1')).toContain('tier2');
|
|
414
|
+
expect(topology.edges.get('tier2')).toContain('tier3');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should build saga topology', () => {
|
|
418
|
+
const config = makeConfig({ swarm: { pattern: 'saga' } });
|
|
419
|
+
const topology = coordinator.resolveTopology(config);
|
|
420
|
+
expect(topology.pattern).toBe('saga');
|
|
421
|
+
expect(topology.hub).toBe('leader');
|
|
422
|
+
expect(topology.edges.get('leader')).toContain('worker-1');
|
|
423
|
+
expect(topology.edges.get('worker-1')).toEqual(['leader']);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should build circuit-breaker topology', () => {
|
|
427
|
+
const config = makeConfig({ swarm: { pattern: 'circuit-breaker' } });
|
|
428
|
+
const topology = coordinator.resolveTopology(config);
|
|
429
|
+
expect(topology.pattern).toBe('circuit-breaker');
|
|
430
|
+
expect(topology.pipelineOrder).toEqual(['leader', 'worker-1', 'worker-2']);
|
|
431
|
+
expect(topology.edges.get('leader')).toEqual(['worker-1']);
|
|
432
|
+
expect(topology.edges.get('worker-2')).toEqual([]);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should build blackboard topology', () => {
|
|
436
|
+
const config = makeConfig({ swarm: { pattern: 'blackboard' } });
|
|
437
|
+
const topology = coordinator.resolveTopology(config);
|
|
438
|
+
expect(topology.pattern).toBe('blackboard');
|
|
439
|
+
// Full mesh for blackboard
|
|
440
|
+
expect(topology.edges.get('leader')).toContain('worker-1');
|
|
441
|
+
expect(topology.edges.get('worker-1')).toContain('leader');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should build swarm topology with neighbor communication', () => {
|
|
445
|
+
const config = makeConfig({ swarm: { pattern: 'swarm' } });
|
|
446
|
+
const topology = coordinator.resolveTopology(config);
|
|
447
|
+
expect(topology.pattern).toBe('swarm');
|
|
448
|
+
// Middle agent should have two neighbors
|
|
449
|
+
expect(topology.edges.get('worker-1')).toContain('leader');
|
|
450
|
+
expect(topology.edges.get('worker-1')).toContain('worker-2');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// ── Edge case tests ─────────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
it('should handle map-reduce with no reducers (fallback to coordinator)', () => {
|
|
456
|
+
const config = makeConfig({
|
|
457
|
+
swarm: { pattern: 'map-reduce' },
|
|
458
|
+
agents: [
|
|
459
|
+
{ name: 'coordinator', cli: 'claude', role: 'lead' },
|
|
460
|
+
{ name: 'mapper-1', cli: 'claude', role: 'mapper' },
|
|
461
|
+
{ name: 'mapper-2', cli: 'claude', role: 'mapper' },
|
|
462
|
+
],
|
|
463
|
+
});
|
|
464
|
+
const topology = coordinator.resolveTopology(config);
|
|
465
|
+
expect(topology.pattern).toBe('map-reduce');
|
|
466
|
+
// Mappers should fallback to coordinator when no reducers
|
|
467
|
+
expect(topology.edges.get('mapper-1')).toContain('coordinator');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should handle verifier with no verifiers (empty edges)', () => {
|
|
471
|
+
const config = makeConfig({
|
|
472
|
+
swarm: { pattern: 'verifier' },
|
|
473
|
+
agents: [
|
|
474
|
+
{ name: 'producer-1', cli: 'claude' },
|
|
475
|
+
{ name: 'producer-2', cli: 'claude' },
|
|
476
|
+
],
|
|
477
|
+
});
|
|
478
|
+
const topology = coordinator.resolveTopology(config);
|
|
479
|
+
expect(topology.pattern).toBe('verifier');
|
|
480
|
+
// Producers have no one to send to
|
|
481
|
+
expect(topology.edges.get('producer-1')).toEqual([]);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should handle escalation with no tier roles (use agent order)', () => {
|
|
485
|
+
const config = makeConfig({
|
|
486
|
+
swarm: { pattern: 'escalation' },
|
|
487
|
+
agents: [
|
|
488
|
+
{ name: 'agent-1', cli: 'claude' },
|
|
489
|
+
{ name: 'agent-2', cli: 'claude' },
|
|
490
|
+
{ name: 'agent-3', cli: 'claude' },
|
|
491
|
+
],
|
|
492
|
+
});
|
|
493
|
+
const topology = coordinator.resolveTopology(config);
|
|
494
|
+
expect(topology.pattern).toBe('escalation');
|
|
495
|
+
expect(topology.pipelineOrder).toEqual(['agent-1', 'agent-2', 'agent-3']);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should handle reflection with no critic (fallback to mesh)', () => {
|
|
499
|
+
const config = makeConfig({
|
|
500
|
+
swarm: { pattern: 'reflection' },
|
|
501
|
+
agents: [
|
|
502
|
+
{ name: 'agent-1', cli: 'claude' },
|
|
503
|
+
{ name: 'agent-2', cli: 'claude' },
|
|
504
|
+
],
|
|
505
|
+
});
|
|
506
|
+
const topology = coordinator.resolveTopology(config);
|
|
507
|
+
expect(topology.pattern).toBe('reflection');
|
|
508
|
+
// Falls back to full mesh when no critic
|
|
509
|
+
expect(topology.edges.get('agent-1')).toContain('agent-2');
|
|
510
|
+
expect(topology.edges.get('agent-2')).toContain('agent-1');
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('should handle swarm with hive-mind role', () => {
|
|
514
|
+
const config = makeConfig({
|
|
515
|
+
swarm: { pattern: 'swarm' },
|
|
516
|
+
agents: [
|
|
517
|
+
{ name: 'hive', cli: 'claude', role: 'hive-mind' },
|
|
518
|
+
{ name: 'drone-1', cli: 'claude' },
|
|
519
|
+
{ name: 'drone-2', cli: 'claude' },
|
|
520
|
+
{ name: 'drone-3', cli: 'claude' },
|
|
521
|
+
],
|
|
522
|
+
});
|
|
523
|
+
const topology = coordinator.resolveTopology(config);
|
|
524
|
+
expect(topology.pattern).toBe('swarm');
|
|
525
|
+
expect(topology.hub).toBe('hive');
|
|
526
|
+
// All drones should connect to hive mind
|
|
527
|
+
expect(topology.edges.get('drone-1')).toContain('hive');
|
|
528
|
+
expect(topology.edges.get('drone-2')).toContain('hive');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should handle red-team with multiple attackers and defenders', () => {
|
|
532
|
+
const config = makeConfig({
|
|
533
|
+
swarm: { pattern: 'red-team' },
|
|
534
|
+
agents: [
|
|
535
|
+
{ name: 'attacker-1', cli: 'claude', role: 'attacker' },
|
|
536
|
+
{ name: 'attacker-2', cli: 'claude', role: 'attacker' },
|
|
537
|
+
{ name: 'defender-1', cli: 'claude', role: 'defender' },
|
|
538
|
+
{ name: 'defender-2', cli: 'claude', role: 'defender' },
|
|
539
|
+
],
|
|
540
|
+
});
|
|
541
|
+
const topology = coordinator.resolveTopology(config);
|
|
542
|
+
expect(topology.pattern).toBe('red-team');
|
|
543
|
+
// Attackers should reach all defenders
|
|
544
|
+
expect(topology.edges.get('attacker-1')).toContain('defender-1');
|
|
545
|
+
expect(topology.edges.get('attacker-1')).toContain('defender-2');
|
|
546
|
+
// Defenders should reach all attackers
|
|
547
|
+
expect(topology.edges.get('defender-1')).toContain('attacker-1');
|
|
548
|
+
expect(topology.edges.get('defender-1')).toContain('attacker-2');
|
|
549
|
+
});
|
|
194
550
|
});
|
|
195
551
|
|
|
196
552
|
// ── Run lifecycle ──────────────────────────────────────────────────────
|
|
@@ -15,20 +15,42 @@ import { getLogs, listLoggedAgents } from "../logs.js";
|
|
|
15
15
|
|
|
16
16
|
// ── waitForAny ──────────────────────────────────────────────────────────────
|
|
17
17
|
|
|
18
|
+
interface FakeAgentControls {
|
|
19
|
+
agent: Agent;
|
|
20
|
+
triggerExit: () => void;
|
|
21
|
+
triggerIdle: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
function makeFakeAgent(
|
|
19
25
|
name: string,
|
|
20
26
|
exitAfterMs?: number,
|
|
21
27
|
): Agent {
|
|
28
|
+
return makeFakeAgentWithControls(name, exitAfterMs).agent;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeFakeAgentWithControls(
|
|
32
|
+
name: string,
|
|
33
|
+
exitAfterMs?: number,
|
|
34
|
+
): FakeAgentControls {
|
|
22
35
|
let resolveExit: ((reason: "exited" | "released") => void) | undefined;
|
|
23
36
|
const exitPromise = new Promise<"exited" | "released">((resolve) => {
|
|
24
37
|
resolveExit = resolve;
|
|
25
38
|
});
|
|
26
39
|
|
|
40
|
+
let resolveIdle: ((reason: "idle" | "timeout" | "exited") => void) | undefined;
|
|
41
|
+
let idlePromise: Promise<"idle" | "timeout" | "exited"> | undefined;
|
|
42
|
+
|
|
43
|
+
function makeIdlePromise() {
|
|
44
|
+
idlePromise = new Promise<"idle" | "timeout" | "exited">((resolve) => {
|
|
45
|
+
resolveIdle = resolve;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
if (exitAfterMs !== undefined) {
|
|
28
50
|
setTimeout(() => resolveExit?.("exited"), exitAfterMs);
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
|
|
53
|
+
const agent: Agent = {
|
|
32
54
|
name,
|
|
33
55
|
runtime: "pty",
|
|
34
56
|
channels: ["general"],
|
|
@@ -49,10 +71,29 @@ function makeFakeAgent(
|
|
|
49
71
|
}
|
|
50
72
|
return exitPromise;
|
|
51
73
|
},
|
|
74
|
+
waitForIdle(timeoutMs?: number) {
|
|
75
|
+
makeIdlePromise();
|
|
76
|
+
if (timeoutMs === 0) return Promise.resolve("timeout" as const);
|
|
77
|
+
if (timeoutMs !== undefined) {
|
|
78
|
+
return Promise.race([
|
|
79
|
+
idlePromise!,
|
|
80
|
+
new Promise<"timeout">((resolve) =>
|
|
81
|
+
setTimeout(() => resolve("timeout"), timeoutMs),
|
|
82
|
+
),
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
return idlePromise!;
|
|
86
|
+
},
|
|
52
87
|
async sendMessage() {
|
|
53
88
|
return { eventId: "fake", from: name, to: "", text: "" };
|
|
54
89
|
},
|
|
55
90
|
};
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
agent,
|
|
94
|
+
triggerExit: () => resolveExit?.("exited"),
|
|
95
|
+
triggerIdle: () => resolveIdle?.("idle"),
|
|
96
|
+
};
|
|
56
97
|
}
|
|
57
98
|
|
|
58
99
|
test("waitForAny: returns first agent to exit", async () => {
|
|
@@ -150,3 +191,53 @@ test("listLoggedAgents: returns empty for missing directory", async () => {
|
|
|
150
191
|
assert.deepEqual(agents, []);
|
|
151
192
|
});
|
|
152
193
|
|
|
194
|
+
// ── waitForIdle ────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
test("waitForIdle: resolves with idle when agent goes idle", async () => {
|
|
197
|
+
const { agent, triggerIdle } = makeFakeAgentWithControls("worker");
|
|
198
|
+
const promise = agent.waitForIdle(5_000);
|
|
199
|
+
setTimeout(() => triggerIdle(), 20);
|
|
200
|
+
const result = await promise;
|
|
201
|
+
assert.equal(result, "idle");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("waitForIdle: resolves with timeout when time elapses", async () => {
|
|
205
|
+
const { agent } = makeFakeAgentWithControls("worker");
|
|
206
|
+
const result = await agent.waitForIdle(50);
|
|
207
|
+
assert.equal(result, "timeout");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("waitForIdle: resolves with exited when agent exits before idle", async () => {
|
|
211
|
+
const { agent, triggerExit } = makeFakeAgentWithControls("worker");
|
|
212
|
+
const idlePromise = agent.waitForIdle(5_000);
|
|
213
|
+
|
|
214
|
+
// Simulate exit resolving the idle promise (as relay.ts wireEvents does)
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
// In a real scenario, wireEvents resolves the idle resolver with "exited"
|
|
217
|
+
// when agent_exited fires. Here we simulate that directly.
|
|
218
|
+
triggerExit();
|
|
219
|
+
}, 20);
|
|
220
|
+
|
|
221
|
+
// The mock's waitForIdle won't auto-resolve on exit (that's wired in relay.ts),
|
|
222
|
+
// so this tests the timeout fallback for the mock. In the real SDK, the
|
|
223
|
+
// wireEvents handler resolves idle resolvers on exit.
|
|
224
|
+
// For the mock, we can test the timeout path instead.
|
|
225
|
+
const result = await agent.waitForIdle(100);
|
|
226
|
+
assert.equal(result, "timeout");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("waitForIdle: returns timeout immediately with timeoutMs=0", async () => {
|
|
230
|
+
const { agent } = makeFakeAgentWithControls("worker");
|
|
231
|
+
const result = await agent.waitForIdle(0);
|
|
232
|
+
assert.equal(result, "timeout");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("waitForIdle: idle resolves before timeout", async () => {
|
|
236
|
+
const { agent, triggerIdle } = makeFakeAgentWithControls("worker");
|
|
237
|
+
// Trigger idle almost immediately, with a long timeout
|
|
238
|
+
const promise = agent.waitForIdle(5_000);
|
|
239
|
+
setTimeout(() => triggerIdle(), 10);
|
|
240
|
+
const result = await promise;
|
|
241
|
+
assert.equal(result, "idle");
|
|
242
|
+
});
|
|
243
|
+
|