@prometheus-ai/swarm-extension 0.5.1
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/CHANGELOG.md +11 -0
- package/README.md +472 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/extension.d.ts +12 -0
- package/dist/types/swarm/dag.d.ts +28 -0
- package/dist/types/swarm/executor.d.ts +24 -0
- package/dist/types/swarm/pipeline.d.ts +39 -0
- package/dist/types/swarm/render.d.ts +2 -0
- package/dist/types/swarm/schema.d.ts +22 -0
- package/dist/types/swarm/state.d.ts +33 -0
- package/package.json +64 -0
- package/src/cli.ts +106 -0
- package/src/extension.ts +256 -0
- package/src/swarm/__tests__/executor.test.ts +68 -0
- package/src/swarm/dag.ts +146 -0
- package/src/swarm/executor.ts +111 -0
- package/src/swarm/pipeline.ts +212 -0
- package/src/swarm/render.ts +63 -0
- package/src/swarm/schema.ts +157 -0
- package/src/swarm/state.ts +127 -0
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@prometheus-ai/swarm-extension",
|
|
4
|
+
"version": "0.5.1",
|
|
5
|
+
"description": "Swarm orchestration extension for prometheus",
|
|
6
|
+
"homepage": "https://prometheus.trivlab.com",
|
|
7
|
+
"author": "Derek Rynd",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/uttamtrivedi/Prometheus.git",
|
|
12
|
+
"directory": "packages/swarm-extension"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/uttamtrivedi/Prometheus/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"swarm",
|
|
19
|
+
"orchestration",
|
|
20
|
+
"agent",
|
|
21
|
+
"extension"
|
|
22
|
+
],
|
|
23
|
+
"main": "./src/extension.ts",
|
|
24
|
+
"types": "./dist/types/extension.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/types/extension.d.ts",
|
|
28
|
+
"import": "./src/extension.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"prometheus-swarm": "src/cli.ts"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"check": "biome check . && bun run check:types",
|
|
36
|
+
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
37
|
+
"lint": "biome lint .",
|
|
38
|
+
"fix": "biome check --write --unsafe .",
|
|
39
|
+
"fmt": "biome format --write ."
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@prometheus-ai/utils": "0.5.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/bun": "^1.3.14"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@prometheus-ai/agent": "0.5.1"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"bun": ">=1.3.14"
|
|
52
|
+
},
|
|
53
|
+
"prometheus": {
|
|
54
|
+
"extensions": [
|
|
55
|
+
"./src/extension.ts"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"files": [
|
|
59
|
+
"src",
|
|
60
|
+
"README.md",
|
|
61
|
+
"CHANGELOG.md",
|
|
62
|
+
"dist/types"
|
|
63
|
+
]
|
|
64
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Direct pipeline runner — executes a swarm pipeline outside of the TUI.
|
|
4
|
+
*
|
|
5
|
+
* Usage: bun cli.ts <path-to-yaml>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { discoverAuthStorage } from "@prometheus-ai/agent";
|
|
11
|
+
import { ModelRegistry } from "@prometheus-ai/agent/config/model-registry";
|
|
12
|
+
import { Settings } from "@prometheus-ai/agent/config/settings";
|
|
13
|
+
import { buildDependencyGraph, buildExecutionWaves, detectCycles } from "./swarm/dag";
|
|
14
|
+
import { PipelineController } from "./swarm/pipeline";
|
|
15
|
+
import { renderSwarmProgress } from "./swarm/render";
|
|
16
|
+
import { parseSwarmYaml, validateSwarmDefinition } from "./swarm/schema";
|
|
17
|
+
import { StateTracker } from "./swarm/state";
|
|
18
|
+
|
|
19
|
+
const yamlPath = process.argv[2];
|
|
20
|
+
if (!yamlPath) {
|
|
21
|
+
console.error("Usage: prometheus-swarm <path-to-yaml>");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const resolvedPath = path.resolve(yamlPath);
|
|
26
|
+
console.log(`Reading: ${resolvedPath}`);
|
|
27
|
+
|
|
28
|
+
const content = await Bun.file(resolvedPath).text();
|
|
29
|
+
const def = parseSwarmYaml(content);
|
|
30
|
+
|
|
31
|
+
console.log(`Swarm: ${def.name}`);
|
|
32
|
+
console.log(`Mode: ${def.mode}`);
|
|
33
|
+
console.log(`Target count: ${def.targetCount}`);
|
|
34
|
+
console.log(`Agents: ${[...def.agents.keys()].join(", ")}`);
|
|
35
|
+
|
|
36
|
+
// Validate
|
|
37
|
+
const errors = validateSwarmDefinition(def);
|
|
38
|
+
if (errors.length > 0) {
|
|
39
|
+
console.error("Validation errors:", errors);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Build DAG
|
|
44
|
+
const deps = buildDependencyGraph(def);
|
|
45
|
+
const cycles = detectCycles(deps);
|
|
46
|
+
if (cycles) {
|
|
47
|
+
console.error("Cycle detected:", cycles);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
const waves = buildExecutionWaves(deps);
|
|
51
|
+
console.log(`Waves: ${waves.map((w, i) => `W${i + 1}:[${w.join(",")}]`).join(" -> ")}`);
|
|
52
|
+
|
|
53
|
+
// Resolve workspace
|
|
54
|
+
const workspace = path.isAbsolute(def.workspace)
|
|
55
|
+
? def.workspace
|
|
56
|
+
: path.resolve(path.dirname(resolvedPath), def.workspace);
|
|
57
|
+
|
|
58
|
+
await fs.mkdir(workspace, { recursive: true });
|
|
59
|
+
console.log(`Workspace: ${workspace}`);
|
|
60
|
+
|
|
61
|
+
// Initialize
|
|
62
|
+
const stateTracker = new StateTracker(workspace, def.name);
|
|
63
|
+
await stateTracker.init([...def.agents.keys()], def.targetCount, def.mode);
|
|
64
|
+
|
|
65
|
+
// Auth + settings
|
|
66
|
+
const authStorage = await discoverAuthStorage();
|
|
67
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
68
|
+
const settings = Settings.isolated();
|
|
69
|
+
|
|
70
|
+
// Progress display
|
|
71
|
+
let lastProgressDump = 0;
|
|
72
|
+
const PROGRESS_INTERVAL_MS = 5000;
|
|
73
|
+
|
|
74
|
+
// Run
|
|
75
|
+
console.log("\n--- Pipeline starting ---\n");
|
|
76
|
+
|
|
77
|
+
const controller = new PipelineController(def, waves, stateTracker);
|
|
78
|
+
const result = await controller.run({
|
|
79
|
+
workspace,
|
|
80
|
+
onProgress: () => {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
if (now - lastProgressDump > PROGRESS_INTERVAL_MS) {
|
|
83
|
+
lastProgressDump = now;
|
|
84
|
+
const lines = renderSwarmProgress(stateTracker.state);
|
|
85
|
+
console.log(lines.join("\n"));
|
|
86
|
+
console.log();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
modelRegistry,
|
|
90
|
+
settings,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log("\n--- Pipeline finished ---\n");
|
|
94
|
+
console.log(`Status: ${result.status}`);
|
|
95
|
+
console.log(`Iterations completed: ${result.iterations}/${def.targetCount}`);
|
|
96
|
+
if (result.errors.length > 0) {
|
|
97
|
+
console.log(`Errors (${result.errors.length}):`);
|
|
98
|
+
for (const err of result.errors) {
|
|
99
|
+
console.log(` - ${err}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
console.log(`\nState saved to: ${stateTracker.swarmDir}`);
|
|
103
|
+
|
|
104
|
+
// Final state dump
|
|
105
|
+
const lines = renderSwarmProgress(stateTracker.state);
|
|
106
|
+
console.log(lines.join("\n"));
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm Extension — Multi-agent pipeline orchestration from YAML definitions.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - /swarm run <file.yaml> — Execute a swarm pipeline
|
|
6
|
+
* - /swarm status — Show current pipeline status
|
|
7
|
+
*
|
|
8
|
+
* Usage: Add this extension's directory to your extensions config,
|
|
9
|
+
* then use /swarm in any prometheus session.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs/promises";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@prometheus-ai/agent";
|
|
15
|
+
import { formatDuration } from "@prometheus-ai/utils";
|
|
16
|
+
import { buildDependencyGraph, buildExecutionWaves, detectCycles } from "./swarm/dag";
|
|
17
|
+
import { PipelineController } from "./swarm/pipeline";
|
|
18
|
+
import { renderSwarmProgress } from "./swarm/render";
|
|
19
|
+
import { parseSwarmYaml, type SwarmDefinition, validateSwarmDefinition } from "./swarm/schema";
|
|
20
|
+
import { StateTracker } from "./swarm/state";
|
|
21
|
+
|
|
22
|
+
export default function swarmExtension(pi: ExtensionAPI): void {
|
|
23
|
+
pi.setLabel("Swarm Orchestrator");
|
|
24
|
+
|
|
25
|
+
pi.registerCommand("swarm", {
|
|
26
|
+
description: "Run a multi-agent swarm pipeline from YAML",
|
|
27
|
+
getArgumentCompletions: prefix => {
|
|
28
|
+
const subcommands = ["run", "status", "help"];
|
|
29
|
+
if (!prefix) return subcommands.map(s => ({ label: s, value: s }));
|
|
30
|
+
return subcommands.filter(s => s.startsWith(prefix)).map(s => ({ label: s, value: s }));
|
|
31
|
+
},
|
|
32
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
33
|
+
const parts = args.trim().split(/\s+/);
|
|
34
|
+
const subcommand = parts[0] ?? "help";
|
|
35
|
+
|
|
36
|
+
switch (subcommand) {
|
|
37
|
+
case "run": {
|
|
38
|
+
const yamlPath = parts[1];
|
|
39
|
+
if (!yamlPath) {
|
|
40
|
+
ctx.ui.notify("Usage: /swarm run <path/to/pipeline.yaml>", "error");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await handleRun(yamlPath, ctx, pi);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
case "status": {
|
|
47
|
+
await handleStatus(parts[1], ctx);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
ctx.ui.notify(
|
|
52
|
+
[
|
|
53
|
+
"Swarm — multi-agent pipeline orchestrator",
|
|
54
|
+
"",
|
|
55
|
+
" /swarm run <file.yaml> Run a pipeline",
|
|
56
|
+
" /swarm status [name] Show pipeline status",
|
|
57
|
+
" /swarm help Show this help",
|
|
58
|
+
].join("\n"),
|
|
59
|
+
"info",
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// /swarm run
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
async function handleRun(yamlPath: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
72
|
+
// 1. Resolve and read YAML
|
|
73
|
+
const resolvedPath = path.isAbsolute(yamlPath) ? yamlPath : path.resolve(ctx.cwd, yamlPath);
|
|
74
|
+
|
|
75
|
+
let content: string;
|
|
76
|
+
try {
|
|
77
|
+
content = await Bun.file(resolvedPath).text();
|
|
78
|
+
} catch {
|
|
79
|
+
ctx.ui.notify(`Cannot read file: ${resolvedPath}`, "error");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Parse YAML
|
|
84
|
+
let def: SwarmDefinition;
|
|
85
|
+
try {
|
|
86
|
+
def = parseSwarmYaml(content);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
ctx.ui.notify(`YAML error: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. Validate
|
|
93
|
+
const validationErrors = validateSwarmDefinition(def);
|
|
94
|
+
if (validationErrors.length > 0) {
|
|
95
|
+
ctx.ui.notify(`Validation errors:\n${validationErrors.map(e => ` - ${e}`).join("\n")}`, "error");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4. Build DAG
|
|
100
|
+
const deps = buildDependencyGraph(def);
|
|
101
|
+
const cycleNodes = detectCycles(deps);
|
|
102
|
+
if (cycleNodes) {
|
|
103
|
+
ctx.ui.notify(`Cycle detected in agent dependencies: [${cycleNodes.join(", ")}]`, "error");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const waves = buildExecutionWaves(deps);
|
|
107
|
+
|
|
108
|
+
// 5. Resolve workspace (relative to YAML file location)
|
|
109
|
+
const workspace = path.isAbsolute(def.workspace)
|
|
110
|
+
? def.workspace
|
|
111
|
+
: path.resolve(path.dirname(resolvedPath), def.workspace);
|
|
112
|
+
|
|
113
|
+
// Ensure workspace exists
|
|
114
|
+
await fs.mkdir(workspace, { recursive: true });
|
|
115
|
+
|
|
116
|
+
// 6. Initialize state tracker
|
|
117
|
+
const stateTracker = new StateTracker(workspace, def.name);
|
|
118
|
+
await stateTracker.init([...def.agents.keys()], def.targetCount, def.mode);
|
|
119
|
+
|
|
120
|
+
// 7. Log start
|
|
121
|
+
const agentList = [...def.agents.keys()].join(", ");
|
|
122
|
+
const waveDesc = waves.map((w, i) => `wave ${i + 1}: [${w.join(", ")}]`).join("; ");
|
|
123
|
+
pi.logger.debug("Swarm starting", {
|
|
124
|
+
name: def.name,
|
|
125
|
+
mode: def.mode,
|
|
126
|
+
agents: agentList,
|
|
127
|
+
waves: waveDesc,
|
|
128
|
+
workspace,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
ctx.ui.notify(
|
|
132
|
+
`Starting swarm '${def.name}': ${def.agents.size} agents, ${waves.length} waves, ${def.targetCount} iteration(s)`,
|
|
133
|
+
"info",
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// 8. Set up progress widget
|
|
137
|
+
const widgetKey = `swarm-${def.name}`;
|
|
138
|
+
const updateWidget = () => {
|
|
139
|
+
const lines = renderSwarmProgress(stateTracker.state);
|
|
140
|
+
ctx.ui.setWidget(widgetKey, lines);
|
|
141
|
+
};
|
|
142
|
+
updateWidget();
|
|
143
|
+
|
|
144
|
+
// 9. Run pipeline
|
|
145
|
+
const controller = new PipelineController(def, waves, stateTracker);
|
|
146
|
+
|
|
147
|
+
const result = await controller.run({
|
|
148
|
+
workspace,
|
|
149
|
+
onProgress: () => updateWidget(),
|
|
150
|
+
modelRegistry: ctx.modelRegistry,
|
|
151
|
+
settings: pi.pi.settings,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// 10. Clear widget and show summary
|
|
155
|
+
ctx.ui.setWidget(widgetKey, undefined);
|
|
156
|
+
|
|
157
|
+
const elapsed = stateTracker.state.completedAt
|
|
158
|
+
? formatDuration(stateTracker.state.completedAt - stateTracker.state.startedAt)
|
|
159
|
+
: "unknown";
|
|
160
|
+
|
|
161
|
+
const summaryParts = [
|
|
162
|
+
`Swarm '${def.name}' ${result.status}`,
|
|
163
|
+
`${result.iterations}/${def.targetCount} iterations`,
|
|
164
|
+
`elapsed: ${elapsed}`,
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
if (result.errors.length > 0) {
|
|
168
|
+
summaryParts.push(`${result.errors.length} error(s)`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const summaryType = result.status === "completed" ? "info" : "error";
|
|
172
|
+
ctx.ui.notify(summaryParts.join(" | "), summaryType);
|
|
173
|
+
|
|
174
|
+
// Log errors
|
|
175
|
+
if (result.errors.length > 0) {
|
|
176
|
+
pi.logger.warn("Swarm completed with errors", { errors: result.errors });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 11. Send summary to the conversation so the LLM knows what happened
|
|
180
|
+
const summaryMessage = buildSummaryMessage(def, result, stateTracker, workspace);
|
|
181
|
+
pi.sendMessage(
|
|
182
|
+
{
|
|
183
|
+
customType: "swarm-result",
|
|
184
|
+
content: [{ type: "text", text: summaryMessage }],
|
|
185
|
+
display: true,
|
|
186
|
+
details: {
|
|
187
|
+
swarmName: def.name,
|
|
188
|
+
status: result.status,
|
|
189
|
+
iterations: result.iterations,
|
|
190
|
+
errorCount: result.errors.length,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{ triggerTurn: false },
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// /swarm status
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
async function handleStatus(name: string | undefined, ctx: ExtensionCommandContext): Promise<void> {
|
|
202
|
+
if (!name) {
|
|
203
|
+
ctx.ui.notify("Usage: /swarm status <name> (reads .swarm_<name>/state/pipeline.json from cwd)", "info");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const stateTracker = new StateTracker(ctx.cwd, name);
|
|
208
|
+
const state = await stateTracker.load();
|
|
209
|
+
if (!state) {
|
|
210
|
+
ctx.ui.notify(`No state found for swarm '${name}' in ${ctx.cwd}`, "error");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const lines = renderSwarmProgress(state);
|
|
215
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ============================================================================
|
|
219
|
+
// Helpers
|
|
220
|
+
// ============================================================================
|
|
221
|
+
|
|
222
|
+
function buildSummaryMessage(
|
|
223
|
+
def: SwarmDefinition,
|
|
224
|
+
result: { status: string; iterations: number; errors: string[] },
|
|
225
|
+
stateTracker: StateTracker,
|
|
226
|
+
workspace: string,
|
|
227
|
+
): string {
|
|
228
|
+
const lines: string[] = [];
|
|
229
|
+
lines.push(`## Swarm Pipeline: ${def.name}`);
|
|
230
|
+
lines.push("");
|
|
231
|
+
lines.push(`- **Status**: ${result.status}`);
|
|
232
|
+
lines.push(`- **Mode**: ${def.mode}`);
|
|
233
|
+
lines.push(`- **Iterations**: ${result.iterations}/${def.targetCount}`);
|
|
234
|
+
lines.push(`- **Workspace**: ${workspace}`);
|
|
235
|
+
lines.push(`- **State dir**: ${stateTracker.swarmDir}`);
|
|
236
|
+
lines.push("");
|
|
237
|
+
|
|
238
|
+
lines.push("### Agent Results");
|
|
239
|
+
lines.push("");
|
|
240
|
+
for (const [name, agent] of Object.entries(stateTracker.state.agents)) {
|
|
241
|
+
const duration =
|
|
242
|
+
agent.startedAt && agent.completedAt ? formatDuration(agent.completedAt - agent.startedAt) : "n/a";
|
|
243
|
+
lines.push(`- **${name}**: ${agent.status} (${duration})${agent.error ? ` — ${agent.error}` : ""}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (result.errors.length > 0) {
|
|
247
|
+
lines.push("");
|
|
248
|
+
lines.push("### Errors");
|
|
249
|
+
lines.push("");
|
|
250
|
+
for (const error of result.errors) {
|
|
251
|
+
lines.push(`- ${error}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { ModelRegistry, SingleResult } from "@prometheus-ai/agent";
|
|
6
|
+
import * as taskExecutor from "@prometheus-ai/agent";
|
|
7
|
+
import { executeSwarmAgent } from "../executor";
|
|
8
|
+
import { StateTracker } from "../state";
|
|
9
|
+
|
|
10
|
+
const mockResult = {
|
|
11
|
+
index: 0,
|
|
12
|
+
id: "test-agent-0",
|
|
13
|
+
agent: "test",
|
|
14
|
+
agentSource: "project",
|
|
15
|
+
task: "test task",
|
|
16
|
+
exitCode: 0,
|
|
17
|
+
output: "ok",
|
|
18
|
+
stderr: "",
|
|
19
|
+
truncated: false,
|
|
20
|
+
durationMs: 100,
|
|
21
|
+
tokens: 0,
|
|
22
|
+
} as SingleResult;
|
|
23
|
+
|
|
24
|
+
let workspace: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
workspace = await fs.mkdtemp(path.join(os.tmpdir(), "swarm-test-"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("executeSwarmAgent", () => {
|
|
36
|
+
it("does not pass authStorage to runSubprocess when modelRegistry is provided", async () => {
|
|
37
|
+
const runSubprocessSpy = vi.spyOn(taskExecutor, "runSubprocess").mockResolvedValue(mockResult);
|
|
38
|
+
|
|
39
|
+
const mockModelRegistry = {
|
|
40
|
+
authStorage: { discover: vi.fn() },
|
|
41
|
+
} as unknown as ModelRegistry;
|
|
42
|
+
|
|
43
|
+
const stateTracker = new StateTracker(workspace, "test-swarm");
|
|
44
|
+
await stateTracker.init(["test-agent"], 1, "parallel");
|
|
45
|
+
|
|
46
|
+
const agent = {
|
|
47
|
+
name: "test-agent",
|
|
48
|
+
role: "tester",
|
|
49
|
+
task: "do something",
|
|
50
|
+
reportsTo: [],
|
|
51
|
+
waitsFor: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
await executeSwarmAgent(agent, 0, {
|
|
55
|
+
workspace,
|
|
56
|
+
swarmName: "test-swarm",
|
|
57
|
+
iteration: 0,
|
|
58
|
+
modelRegistry: mockModelRegistry,
|
|
59
|
+
stateTracker,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(runSubprocessSpy).toHaveBeenCalledTimes(1);
|
|
63
|
+
const passedOptions = runSubprocessSpy.mock.calls[0][0];
|
|
64
|
+
const { authStorage, modelRegistry } = passedOptions;
|
|
65
|
+
expect(authStorage).toBeUndefined();
|
|
66
|
+
expect(modelRegistry).toBe(mockModelRegistry);
|
|
67
|
+
});
|
|
68
|
+
});
|
package/src/swarm/dag.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directed Acyclic Graph operations for swarm agent dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Builds a dependency graph from waits_for / reports_to relationships,
|
|
5
|
+
* detects cycles, and produces execution waves via topological sort.
|
|
6
|
+
*/
|
|
7
|
+
import type { SwarmDefinition } from "./schema";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a dependency map: agent name → set of agents it depends on.
|
|
11
|
+
*
|
|
12
|
+
* Dependencies come from:
|
|
13
|
+
* 1. Explicit `waits_for` declarations
|
|
14
|
+
* 2. Implicit from `reports_to` (if A reports_to B, then B depends on A)
|
|
15
|
+
* 3. For pipeline/sequential mode with no explicit deps: chain by YAML declaration order
|
|
16
|
+
*/
|
|
17
|
+
export function buildDependencyGraph(def: SwarmDefinition): Map<string, Set<string>> {
|
|
18
|
+
const deps = new Map<string, Set<string>>();
|
|
19
|
+
|
|
20
|
+
for (const name of def.agents.keys()) {
|
|
21
|
+
deps.set(name, new Set());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Explicit waits_for
|
|
25
|
+
for (const [name, agent] of def.agents) {
|
|
26
|
+
for (const dep of agent.waitsFor) {
|
|
27
|
+
if (deps.has(dep)) {
|
|
28
|
+
deps.get(name)!.add(dep);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// reports_to implies the target waits for the reporter
|
|
34
|
+
for (const [name, agent] of def.agents) {
|
|
35
|
+
for (const target of agent.reportsTo) {
|
|
36
|
+
if (deps.has(target)) {
|
|
37
|
+
deps.get(target)!.add(name);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// For pipeline/sequential with no explicit deps, chain by declaration order
|
|
43
|
+
if ((def.mode === "pipeline" || def.mode === "sequential") && !hasExplicitDeps(deps)) {
|
|
44
|
+
for (let i = 1; i < def.agentOrder.length; i++) {
|
|
45
|
+
deps.get(def.agentOrder[i])!.add(def.agentOrder[i - 1]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return deps;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasExplicitDeps(deps: Map<string, Set<string>>): boolean {
|
|
53
|
+
for (const s of deps.values()) {
|
|
54
|
+
if (s.size > 0) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Detect cycles in the dependency graph.
|
|
61
|
+
* Returns the names of agents involved in cycles, or null if acyclic.
|
|
62
|
+
*/
|
|
63
|
+
export function detectCycles(deps: Map<string, Set<string>>): string[] | null {
|
|
64
|
+
// Kahn's algorithm: if topological sort doesn't include all nodes, cycles exist
|
|
65
|
+
const inDegree = new Map<string, number>();
|
|
66
|
+
const forward = new Map<string, string[]>(); // dependency → its dependents
|
|
67
|
+
|
|
68
|
+
for (const [node, nodeDeps] of deps) {
|
|
69
|
+
inDegree.set(node, nodeDeps.size);
|
|
70
|
+
for (const dep of nodeDeps) {
|
|
71
|
+
const list = forward.get(dep) ?? [];
|
|
72
|
+
list.push(node);
|
|
73
|
+
forward.set(dep, list);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const queue: string[] = [];
|
|
78
|
+
for (const [node, degree] of inDegree) {
|
|
79
|
+
if (degree === 0) queue.push(node);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sorted: string[] = [];
|
|
83
|
+
while (queue.length > 0) {
|
|
84
|
+
const node = queue.shift()!;
|
|
85
|
+
sorted.push(node);
|
|
86
|
+
for (const dependent of forward.get(node) ?? []) {
|
|
87
|
+
const newDegree = inDegree.get(dependent)! - 1;
|
|
88
|
+
inDegree.set(dependent, newDegree);
|
|
89
|
+
if (newDegree === 0) queue.push(dependent);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (sorted.length < deps.size) {
|
|
94
|
+
return [...deps.keys()].filter(k => !sorted.includes(k));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build execution waves from dependency graph via topological sort.
|
|
102
|
+
*
|
|
103
|
+
* Each wave contains agents whose dependencies are all in earlier waves.
|
|
104
|
+
* Agents within a wave can execute in parallel.
|
|
105
|
+
*/
|
|
106
|
+
export function buildExecutionWaves(deps: Map<string, Set<string>>): string[][] {
|
|
107
|
+
const waves: string[][] = [];
|
|
108
|
+
const completed = new Set<string>();
|
|
109
|
+
const remaining = new Set(deps.keys());
|
|
110
|
+
|
|
111
|
+
while (remaining.size > 0) {
|
|
112
|
+
const wave: string[] = [];
|
|
113
|
+
|
|
114
|
+
for (const node of remaining) {
|
|
115
|
+
const nodeDeps = deps.get(node)!;
|
|
116
|
+
let ready = true;
|
|
117
|
+
for (const dep of nodeDeps) {
|
|
118
|
+
if (!completed.has(dep)) {
|
|
119
|
+
ready = false;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (ready) {
|
|
124
|
+
wave.push(node);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (wave.length === 0) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Deadlock: agents [${[...remaining].join(", ")}] cannot make progress. This indicates a bug in cycle detection.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Sort for deterministic execution order
|
|
135
|
+
wave.sort();
|
|
136
|
+
|
|
137
|
+
for (const node of wave) {
|
|
138
|
+
remaining.delete(node);
|
|
139
|
+
completed.add(node);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
waves.push(wave);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return waves;
|
|
146
|
+
}
|