swarm-code 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/bin/swarm.mjs +45 -0
- package/dist/agents/aider.d.ts +12 -0
- package/dist/agents/aider.js +182 -0
- package/dist/agents/claude-code.d.ts +9 -0
- package/dist/agents/claude-code.js +216 -0
- package/dist/agents/codex.d.ts +14 -0
- package/dist/agents/codex.js +193 -0
- package/dist/agents/direct-llm.d.ts +9 -0
- package/dist/agents/direct-llm.js +78 -0
- package/dist/agents/mock.d.ts +9 -0
- package/dist/agents/mock.js +77 -0
- package/dist/agents/opencode.d.ts +23 -0
- package/dist/agents/opencode.js +571 -0
- package/dist/agents/provider.d.ts +11 -0
- package/dist/agents/provider.js +31 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +285 -0
- package/dist/compression/compressor.d.ts +28 -0
- package/dist/compression/compressor.js +265 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +170 -0
- package/dist/core/repl.d.ts +69 -0
- package/dist/core/repl.js +336 -0
- package/dist/core/rlm.d.ts +63 -0
- package/dist/core/rlm.js +409 -0
- package/dist/core/runtime.py +335 -0
- package/dist/core/types.d.ts +131 -0
- package/dist/core/types.js +19 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +75 -0
- package/dist/interactive-swarm.d.ts +20 -0
- package/dist/interactive-swarm.js +1041 -0
- package/dist/interactive.d.ts +10 -0
- package/dist/interactive.js +1765 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +242 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.js +72 -0
- package/dist/mcp/session.d.ts +73 -0
- package/dist/mcp/session.js +184 -0
- package/dist/mcp/tools.d.ts +15 -0
- package/dist/mcp/tools.js +377 -0
- package/dist/memory/episodic.d.ts +132 -0
- package/dist/memory/episodic.js +390 -0
- package/dist/prompts/orchestrator.d.ts +5 -0
- package/dist/prompts/orchestrator.js +191 -0
- package/dist/routing/model-router.d.ts +130 -0
- package/dist/routing/model-router.js +515 -0
- package/dist/swarm.d.ts +14 -0
- package/dist/swarm.js +557 -0
- package/dist/threads/cache.d.ts +58 -0
- package/dist/threads/cache.js +198 -0
- package/dist/threads/manager.d.ts +85 -0
- package/dist/threads/manager.js +659 -0
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +42 -0
- package/dist/ui/dashboard.d.ts +33 -0
- package/dist/ui/dashboard.js +151 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.js +11 -0
- package/dist/ui/log.d.ts +39 -0
- package/dist/ui/log.js +126 -0
- package/dist/ui/onboarding.d.ts +14 -0
- package/dist/ui/onboarding.js +518 -0
- package/dist/ui/spinner.d.ts +25 -0
- package/dist/ui/spinner.js +113 -0
- package/dist/ui/summary.d.ts +18 -0
- package/dist/ui/summary.js +113 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.js +97 -0
- package/dist/viewer.d.ts +12 -0
- package/dist/viewer.js +1284 -0
- package/dist/worktree/manager.d.ts +45 -0
- package/dist/worktree/manager.js +266 -0
- package/dist/worktree/merge.d.ts +28 -0
- package/dist/worktree/merge.js +138 -0
- package/package.json +69 -0
package/dist/swarm.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm mode — orchestrates coding agents in parallel via the RLM loop.
|
|
3
|
+
*
|
|
4
|
+
* Usage: swarm --dir ./my-project "add error handling to all API routes"
|
|
5
|
+
*
|
|
6
|
+
* This module:
|
|
7
|
+
* 1. Parses swarm-specific CLI args
|
|
8
|
+
* 2. Scans the target directory to build a codebase context
|
|
9
|
+
* 3. Sets up ThreadManager + WorktreeManager
|
|
10
|
+
* 4. Runs the RLM loop with the swarm orchestrator prompt
|
|
11
|
+
* 5. Cleans up worktrees on exit
|
|
12
|
+
*/
|
|
13
|
+
import "./env.js";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
// Dynamic imports — ensures env.js has set process.env BEFORE pi-ai loads
|
|
17
|
+
const { getModels, getProviders } = await import("@mariozechner/pi-ai");
|
|
18
|
+
const { PythonRepl } = await import("./core/repl.js");
|
|
19
|
+
const { runRlmLoop } = await import("./core/rlm.js");
|
|
20
|
+
const { loadConfig } = await import("./config.js");
|
|
21
|
+
// Register agent backends
|
|
22
|
+
const opencodeMod = await import("./agents/opencode.js");
|
|
23
|
+
await import("./agents/direct-llm.js");
|
|
24
|
+
await import("./agents/claude-code.js");
|
|
25
|
+
await import("./agents/codex.js");
|
|
26
|
+
await import("./agents/aider.js");
|
|
27
|
+
import { randomBytes } from "node:crypto";
|
|
28
|
+
import { EpisodicMemory } from "./memory/episodic.js";
|
|
29
|
+
import { buildSwarmSystemPrompt } from "./prompts/orchestrator.js";
|
|
30
|
+
import { classifyTaskComplexity, describeAvailableAgents, FailureTracker, routeTask } from "./routing/model-router.js";
|
|
31
|
+
import { ThreadManager } from "./threads/manager.js";
|
|
32
|
+
import { renderBanner } from "./ui/banner.js";
|
|
33
|
+
import { ThreadDashboard } from "./ui/dashboard.js";
|
|
34
|
+
import { isJsonMode, logAnswer, logError, logRouter, logSuccess, logVerbose, logWarn, setJsonMode, setLogLevel, } from "./ui/log.js";
|
|
35
|
+
import { runOnboarding } from "./ui/onboarding.js";
|
|
36
|
+
// UI system
|
|
37
|
+
import { Spinner } from "./ui/spinner.js";
|
|
38
|
+
import { renderSummary } from "./ui/summary.js";
|
|
39
|
+
import { mergeAllThreads } from "./worktree/merge.js";
|
|
40
|
+
function parseSwarmArgs(args) {
|
|
41
|
+
let dir = "";
|
|
42
|
+
let orchestratorModel = "";
|
|
43
|
+
let agent = "";
|
|
44
|
+
let dryRun = false;
|
|
45
|
+
let maxBudget = null;
|
|
46
|
+
let verbose = false;
|
|
47
|
+
let quiet = false;
|
|
48
|
+
let json = false;
|
|
49
|
+
let autoRoute = false;
|
|
50
|
+
const positional = [];
|
|
51
|
+
for (let i = 0; i < args.length; i++) {
|
|
52
|
+
const arg = args[i];
|
|
53
|
+
if (arg === "--help" || arg === "-h") {
|
|
54
|
+
process.stderr.write(`\nUsage: swarm --dir <path> [options] "your task"\n\n`);
|
|
55
|
+
process.stderr.write(`Options:\n`);
|
|
56
|
+
process.stderr.write(` --dir <path> Target repository directory\n`);
|
|
57
|
+
process.stderr.write(` --orchestrator <model> Orchestrator LLM model\n`);
|
|
58
|
+
process.stderr.write(` --agent <backend> Agent backend (opencode, claude, codex, aider)\n`);
|
|
59
|
+
process.stderr.write(` --dry-run Plan only, don't spawn threads\n`);
|
|
60
|
+
process.stderr.write(` --max-budget <usd> Maximum session budget\n`);
|
|
61
|
+
process.stderr.write(` --auto-route Enable automatic model selection\n`);
|
|
62
|
+
process.stderr.write(` --verbose Detailed progress output\n`);
|
|
63
|
+
process.stderr.write(` --quiet / -q Suppress non-essential output\n`);
|
|
64
|
+
process.stderr.write(` --json Machine-readable JSON output\n\n`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
else if (arg === "--dir" && i + 1 < args.length) {
|
|
68
|
+
dir = args[++i];
|
|
69
|
+
}
|
|
70
|
+
else if (arg === "--orchestrator" && i + 1 < args.length) {
|
|
71
|
+
orchestratorModel = args[++i];
|
|
72
|
+
}
|
|
73
|
+
else if (arg === "--agent" && i + 1 < args.length) {
|
|
74
|
+
agent = args[++i];
|
|
75
|
+
}
|
|
76
|
+
else if (arg === "--dry-run") {
|
|
77
|
+
dryRun = true;
|
|
78
|
+
}
|
|
79
|
+
else if (arg === "--max-budget" && i + 1 < args.length) {
|
|
80
|
+
const rawBudget = args[++i];
|
|
81
|
+
const parsed = parseFloat(rawBudget);
|
|
82
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
83
|
+
maxBudget = parsed;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
logWarn(`Invalid --max-budget value "${rawBudget}", ignoring`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (arg === "--verbose") {
|
|
90
|
+
verbose = true;
|
|
91
|
+
}
|
|
92
|
+
else if (arg === "--quiet" || arg === "-q") {
|
|
93
|
+
quiet = true;
|
|
94
|
+
}
|
|
95
|
+
else if (arg === "--json") {
|
|
96
|
+
json = true;
|
|
97
|
+
}
|
|
98
|
+
else if (arg === "--auto-route") {
|
|
99
|
+
autoRoute = true;
|
|
100
|
+
}
|
|
101
|
+
else if (arg.startsWith("--")) {
|
|
102
|
+
logWarn(`Unknown flag: ${arg}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
positional.push(arg);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!dir) {
|
|
109
|
+
logError("--dir <path> is required for swarm mode", 'Usage: swarm --dir ./project "your task"');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
if (positional.length === 0) {
|
|
113
|
+
logError("Query argument is required", 'Usage: swarm --dir ./project "your task description"');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
dir: path.resolve(dir),
|
|
118
|
+
orchestratorModel: orchestratorModel || process.env.RLM_MODEL || "claude-sonnet-4-6",
|
|
119
|
+
agent: agent || "",
|
|
120
|
+
dryRun,
|
|
121
|
+
maxBudget,
|
|
122
|
+
autoRoute,
|
|
123
|
+
verbose,
|
|
124
|
+
quiet,
|
|
125
|
+
json,
|
|
126
|
+
query: positional.join(" "),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// ── Codebase scanning ───────────────────────────────────────────────────────
|
|
130
|
+
const SKIP_DIRS = new Set([
|
|
131
|
+
"node_modules",
|
|
132
|
+
".git",
|
|
133
|
+
"dist",
|
|
134
|
+
"build",
|
|
135
|
+
".next",
|
|
136
|
+
".venv",
|
|
137
|
+
"venv",
|
|
138
|
+
"__pycache__",
|
|
139
|
+
".swarm-worktrees",
|
|
140
|
+
"coverage",
|
|
141
|
+
".turbo",
|
|
142
|
+
".cache",
|
|
143
|
+
]);
|
|
144
|
+
const SKIP_EXTENSIONS = new Set([
|
|
145
|
+
".png",
|
|
146
|
+
".jpg",
|
|
147
|
+
".jpeg",
|
|
148
|
+
".gif",
|
|
149
|
+
".ico",
|
|
150
|
+
".svg",
|
|
151
|
+
".woff",
|
|
152
|
+
".woff2",
|
|
153
|
+
".ttf",
|
|
154
|
+
".eot",
|
|
155
|
+
".mp3",
|
|
156
|
+
".mp4",
|
|
157
|
+
".webm",
|
|
158
|
+
".zip",
|
|
159
|
+
".tar",
|
|
160
|
+
".gz",
|
|
161
|
+
".lock",
|
|
162
|
+
".map",
|
|
163
|
+
]);
|
|
164
|
+
function scanDirectory(dir, maxFiles = 200, maxTotalSize = 2 * 1024 * 1024) {
|
|
165
|
+
const files = [];
|
|
166
|
+
let totalSize = 0;
|
|
167
|
+
function walk(currentDir, depth) {
|
|
168
|
+
if (depth > 15 || files.length >= maxFiles || totalSize >= maxTotalSize)
|
|
169
|
+
return;
|
|
170
|
+
let entries;
|
|
171
|
+
try {
|
|
172
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (files.length >= maxFiles || totalSize >= maxTotalSize)
|
|
180
|
+
return;
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
183
|
+
walk(path.join(currentDir, entry.name), depth + 1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else if (entry.isFile()) {
|
|
187
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
188
|
+
if (SKIP_EXTENSIONS.has(ext))
|
|
189
|
+
continue;
|
|
190
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
191
|
+
try {
|
|
192
|
+
const stat = fs.statSync(fullPath);
|
|
193
|
+
if (stat.size > 100 * 1024)
|
|
194
|
+
continue;
|
|
195
|
+
if (stat.size === 0)
|
|
196
|
+
continue;
|
|
197
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
198
|
+
if (content.includes("\0"))
|
|
199
|
+
continue;
|
|
200
|
+
const relPath = path.relative(dir, fullPath);
|
|
201
|
+
files.push({ relPath, content });
|
|
202
|
+
totalSize += content.length;
|
|
203
|
+
}
|
|
204
|
+
catch { }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
walk(dir, 0);
|
|
209
|
+
const parts = [];
|
|
210
|
+
parts.push(`Codebase: ${path.basename(dir)}`);
|
|
211
|
+
parts.push(`Files: ${files.length}`);
|
|
212
|
+
parts.push(`Total size: ${(totalSize / 1024).toFixed(1)}KB`);
|
|
213
|
+
parts.push("---");
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
parts.push(`\n=== ${file.relPath} ===`);
|
|
216
|
+
parts.push(file.content);
|
|
217
|
+
}
|
|
218
|
+
return parts.join("\n");
|
|
219
|
+
}
|
|
220
|
+
// ── Model resolution ────────────────────────────────────────────────────────
|
|
221
|
+
function resolveModel(modelId) {
|
|
222
|
+
const providerKeys = {
|
|
223
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
224
|
+
openai: "OPENAI_API_KEY",
|
|
225
|
+
google: "GEMINI_API_KEY",
|
|
226
|
+
};
|
|
227
|
+
const defaultModels = {
|
|
228
|
+
anthropic: "claude-sonnet-4-6",
|
|
229
|
+
openai: "gpt-4o",
|
|
230
|
+
google: "gemini-2.5-flash",
|
|
231
|
+
};
|
|
232
|
+
const knownProviders = new Set(Object.keys(providerKeys));
|
|
233
|
+
let model;
|
|
234
|
+
let resolvedProvider = "";
|
|
235
|
+
for (const provider of getProviders()) {
|
|
236
|
+
if (!knownProviders.has(provider))
|
|
237
|
+
continue;
|
|
238
|
+
const key = providerKeys[provider];
|
|
239
|
+
if (!process.env[key])
|
|
240
|
+
continue;
|
|
241
|
+
for (const m of getModels(provider)) {
|
|
242
|
+
if (m.id === modelId) {
|
|
243
|
+
model = m;
|
|
244
|
+
resolvedProvider = provider;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (model)
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (!model) {
|
|
252
|
+
for (const provider of getProviders()) {
|
|
253
|
+
if (knownProviders.has(provider))
|
|
254
|
+
continue;
|
|
255
|
+
for (const m of getModels(provider)) {
|
|
256
|
+
if (m.id === modelId) {
|
|
257
|
+
model = m;
|
|
258
|
+
resolvedProvider = provider;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (model)
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!model) {
|
|
267
|
+
for (const [prov, envKey] of Object.entries(providerKeys)) {
|
|
268
|
+
if (!process.env[envKey])
|
|
269
|
+
continue;
|
|
270
|
+
const fallbackId = defaultModels[prov];
|
|
271
|
+
if (!fallbackId)
|
|
272
|
+
continue;
|
|
273
|
+
for (const p of getProviders()) {
|
|
274
|
+
if (p !== prov)
|
|
275
|
+
continue;
|
|
276
|
+
for (const m of getModels(p)) {
|
|
277
|
+
if (m.id === fallbackId) {
|
|
278
|
+
model = m;
|
|
279
|
+
resolvedProvider = prov;
|
|
280
|
+
logWarn(`Using ${fallbackId} (${prov}) — model "${modelId}" not found`);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (model)
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
if (model)
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!model)
|
|
292
|
+
return null;
|
|
293
|
+
return { model, provider: resolvedProvider };
|
|
294
|
+
}
|
|
295
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
296
|
+
export async function runSwarmMode(rawArgs) {
|
|
297
|
+
const args = parseSwarmArgs(rawArgs);
|
|
298
|
+
const config = loadConfig();
|
|
299
|
+
// Configure UI
|
|
300
|
+
if (args.json)
|
|
301
|
+
setJsonMode(true);
|
|
302
|
+
if (args.quiet)
|
|
303
|
+
setLogLevel("quiet");
|
|
304
|
+
else if (args.verbose)
|
|
305
|
+
setLogLevel("verbose");
|
|
306
|
+
// Verify target directory before anything else
|
|
307
|
+
if (!fs.existsSync(args.dir)) {
|
|
308
|
+
logError(`Directory "${args.dir}" does not exist`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
// First-run onboarding (after dir validation so we don't waste user's time)
|
|
312
|
+
await runOnboarding();
|
|
313
|
+
// Override config with CLI args
|
|
314
|
+
if (args.agent)
|
|
315
|
+
config.default_agent = args.agent;
|
|
316
|
+
if (args.maxBudget !== null)
|
|
317
|
+
config.max_session_budget_usd = args.maxBudget;
|
|
318
|
+
if (args.autoRoute)
|
|
319
|
+
config.auto_model_selection = true;
|
|
320
|
+
// Resolve orchestrator model
|
|
321
|
+
const resolved = resolveModel(args.orchestratorModel);
|
|
322
|
+
if (!resolved) {
|
|
323
|
+
logError(`Could not find model "${args.orchestratorModel}"`, "Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY in your .env file");
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
// Initialize episodic memory if enabled
|
|
327
|
+
let episodicMemory;
|
|
328
|
+
if (config.episodic_memory_enabled) {
|
|
329
|
+
episodicMemory = new EpisodicMemory(config.memory_dir);
|
|
330
|
+
await episodicMemory.init();
|
|
331
|
+
}
|
|
332
|
+
// Initialize failure tracker for session-level agent failure tracking
|
|
333
|
+
const failureTracker = new FailureTracker();
|
|
334
|
+
// Render banner
|
|
335
|
+
renderBanner({
|
|
336
|
+
dir: args.dir,
|
|
337
|
+
model: resolved.model.id,
|
|
338
|
+
provider: resolved.provider,
|
|
339
|
+
agent: config.default_agent,
|
|
340
|
+
routing: config.auto_model_selection ? "auto" : "orchestrator-driven",
|
|
341
|
+
query: args.query,
|
|
342
|
+
dryRun: args.dryRun,
|
|
343
|
+
memorySize: episodicMemory?.size,
|
|
344
|
+
});
|
|
345
|
+
// Scan codebase with spinner
|
|
346
|
+
const spinner = new Spinner();
|
|
347
|
+
spinner.start("scanning codebase");
|
|
348
|
+
const context = scanDirectory(args.dir);
|
|
349
|
+
spinner.stop();
|
|
350
|
+
logSuccess(`Scanned codebase — ${(context.length / 1024).toFixed(1)}KB context`);
|
|
351
|
+
// Start REPL
|
|
352
|
+
const repl = new PythonRepl();
|
|
353
|
+
const ac = new AbortController();
|
|
354
|
+
// Thread dashboard for live status
|
|
355
|
+
const dashboard = new ThreadDashboard();
|
|
356
|
+
// Progress callback for thread events
|
|
357
|
+
const threadProgress = (threadId, phase, detail) => {
|
|
358
|
+
if (phase === "completed" || phase === "failed" || phase === "cancelled") {
|
|
359
|
+
dashboard.complete(threadId, phase, detail);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
dashboard.update(threadId, phase, detail);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
// Enable OpenCode server mode for persistent connections (reduces cold-start)
|
|
366
|
+
if (config.default_agent === "opencode" && config.opencode_server_mode) {
|
|
367
|
+
opencodeMod.enableServerMode();
|
|
368
|
+
logVerbose("OpenCode server mode enabled");
|
|
369
|
+
}
|
|
370
|
+
// Initialize thread manager
|
|
371
|
+
const threadManager = new ThreadManager(args.dir, config, threadProgress, ac.signal);
|
|
372
|
+
await threadManager.init();
|
|
373
|
+
if (episodicMemory) {
|
|
374
|
+
threadManager.setEpisodicMemory(episodicMemory);
|
|
375
|
+
}
|
|
376
|
+
const abortAndExit = () => {
|
|
377
|
+
spinner.stop();
|
|
378
|
+
dashboard.clear();
|
|
379
|
+
logWarn("Aborting...");
|
|
380
|
+
ac.abort();
|
|
381
|
+
};
|
|
382
|
+
process.on("SIGINT", abortAndExit);
|
|
383
|
+
process.on("SIGTERM", abortAndExit);
|
|
384
|
+
try {
|
|
385
|
+
await repl.start(ac.signal);
|
|
386
|
+
// Register LLM summarizer for llm-summary compression strategy
|
|
387
|
+
if (config.compression_strategy === "llm-summary") {
|
|
388
|
+
const { setSummarizer } = await import("./compression/compressor.js");
|
|
389
|
+
const { completeSimple } = await import("@mariozechner/pi-ai");
|
|
390
|
+
setSummarizer(async (text, instruction) => {
|
|
391
|
+
const response = await completeSimple(resolved.model, {
|
|
392
|
+
systemPrompt: instruction,
|
|
393
|
+
messages: [
|
|
394
|
+
{
|
|
395
|
+
role: "user",
|
|
396
|
+
content: text,
|
|
397
|
+
timestamp: Date.now(),
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
});
|
|
401
|
+
return response.content
|
|
402
|
+
.filter((b) => b.type === "text")
|
|
403
|
+
.map((b) => b.text)
|
|
404
|
+
.join("");
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
// Build system prompt
|
|
408
|
+
const agentDesc = await describeAvailableAgents();
|
|
409
|
+
let systemPrompt = buildSwarmSystemPrompt(config, agentDesc);
|
|
410
|
+
if (args.dryRun) {
|
|
411
|
+
systemPrompt +=
|
|
412
|
+
"\n\n## DRY RUN MODE\nDo NOT call thread() or async_thread(). Instead, describe what threads you WOULD spawn (task, files, model). Call FINAL() with your execution plan.";
|
|
413
|
+
}
|
|
414
|
+
// Add episodic memory hints
|
|
415
|
+
if (episodicMemory && episodicMemory.size > 0) {
|
|
416
|
+
const hints = episodicMemory.getStrategyHints(args.query);
|
|
417
|
+
if (hints) {
|
|
418
|
+
systemPrompt += `\n\n## Episodic Memory\n${hints}\nConsider these strategies when decomposing your task.`;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Thread handler
|
|
422
|
+
const threadHandler = async (task, threadContext, agentBackend, model, files) => {
|
|
423
|
+
let resolvedAgent = agentBackend || config.default_agent;
|
|
424
|
+
let resolvedModel = model || config.default_model;
|
|
425
|
+
let routeSlot = "";
|
|
426
|
+
let routeComplexity = "";
|
|
427
|
+
if (config.auto_model_selection && !agentBackend && !model) {
|
|
428
|
+
const route = await routeTask(task, config, episodicMemory, failureTracker);
|
|
429
|
+
resolvedAgent = route.agent;
|
|
430
|
+
resolvedModel = route.model;
|
|
431
|
+
routeSlot = route.slot;
|
|
432
|
+
routeComplexity = classifyTaskComplexity(task);
|
|
433
|
+
logRouter(`${route.reason} [slot: ${route.slot}]`);
|
|
434
|
+
}
|
|
435
|
+
const threadId = randomBytes(6).toString("hex");
|
|
436
|
+
// Update dashboard with task info
|
|
437
|
+
dashboard.update(threadId, "queued", undefined, {
|
|
438
|
+
task,
|
|
439
|
+
agent: resolvedAgent,
|
|
440
|
+
model: resolvedModel,
|
|
441
|
+
});
|
|
442
|
+
const result = await threadManager.spawnThread({
|
|
443
|
+
id: threadId,
|
|
444
|
+
task,
|
|
445
|
+
context: threadContext,
|
|
446
|
+
agent: {
|
|
447
|
+
backend: resolvedAgent,
|
|
448
|
+
model: resolvedModel,
|
|
449
|
+
},
|
|
450
|
+
files,
|
|
451
|
+
});
|
|
452
|
+
// Track failure in the failure tracker for routing adjustments
|
|
453
|
+
if (!result.success) {
|
|
454
|
+
failureTracker.recordFailure(resolvedAgent, resolvedModel, task, result.summary || "unknown error");
|
|
455
|
+
}
|
|
456
|
+
// Record episode
|
|
457
|
+
if (episodicMemory && result.success && routeSlot) {
|
|
458
|
+
episodicMemory
|
|
459
|
+
.record({
|
|
460
|
+
task,
|
|
461
|
+
agent: resolvedAgent,
|
|
462
|
+
model: resolvedModel,
|
|
463
|
+
slot: routeSlot,
|
|
464
|
+
complexity: routeComplexity,
|
|
465
|
+
success: true,
|
|
466
|
+
durationMs: result.durationMs,
|
|
467
|
+
estimatedCostUsd: result.estimatedCostUsd,
|
|
468
|
+
filesChanged: result.filesChanged,
|
|
469
|
+
summary: result.summary,
|
|
470
|
+
})
|
|
471
|
+
.catch(() => { });
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
result: result.summary,
|
|
475
|
+
success: result.success,
|
|
476
|
+
filesChanged: result.filesChanged,
|
|
477
|
+
durationMs: result.durationMs,
|
|
478
|
+
};
|
|
479
|
+
};
|
|
480
|
+
// Merge handler
|
|
481
|
+
const mergeHandler = async () => {
|
|
482
|
+
spinner.update("merging thread branches");
|
|
483
|
+
const threads = threadManager.getThreads();
|
|
484
|
+
const mergeOpts = { continueOnConflict: true };
|
|
485
|
+
const results = await mergeAllThreads(args.dir, threads, mergeOpts);
|
|
486
|
+
const merged = results.filter((r) => r.success).length;
|
|
487
|
+
const failed = results.filter((r) => !r.success).length;
|
|
488
|
+
if (failed > 0) {
|
|
489
|
+
logWarn(`Merged ${merged} branches, ${failed} failed`);
|
|
490
|
+
}
|
|
491
|
+
else if (merged > 0) {
|
|
492
|
+
logSuccess(`Merged ${merged} branches`);
|
|
493
|
+
}
|
|
494
|
+
const summary = results
|
|
495
|
+
.map((r) => (r.success ? `Merged ${r.branch}: ${r.message}` : `FAILED ${r.branch}: ${r.message}`))
|
|
496
|
+
.join("\n");
|
|
497
|
+
return {
|
|
498
|
+
result: summary || "No threads to merge",
|
|
499
|
+
success: results.every((r) => r.success),
|
|
500
|
+
};
|
|
501
|
+
};
|
|
502
|
+
// Run the orchestrator
|
|
503
|
+
spinner.start();
|
|
504
|
+
const startTime = Date.now();
|
|
505
|
+
const result = await runRlmLoop({
|
|
506
|
+
context,
|
|
507
|
+
query: args.query,
|
|
508
|
+
model: resolved.model,
|
|
509
|
+
repl,
|
|
510
|
+
signal: ac.signal,
|
|
511
|
+
systemPrompt,
|
|
512
|
+
threadHandler: args.dryRun ? undefined : threadHandler,
|
|
513
|
+
mergeHandler: args.dryRun ? undefined : mergeHandler,
|
|
514
|
+
onProgress: (info) => {
|
|
515
|
+
spinner.update(`iteration ${info.iteration}/${info.maxIterations}` +
|
|
516
|
+
(info.subQueries > 0 ? ` · ${info.subQueries} queries` : ""));
|
|
517
|
+
logVerbose(`Iteration ${info.iteration}/${info.maxIterations} | ` +
|
|
518
|
+
`Sub-queries: ${info.subQueries} | Phase: ${info.phase}`);
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
spinner.stop();
|
|
522
|
+
dashboard.clear();
|
|
523
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
524
|
+
// Render summary
|
|
525
|
+
const summary = {
|
|
526
|
+
elapsed,
|
|
527
|
+
iterations: result.iterations,
|
|
528
|
+
subQueries: result.totalSubQueries,
|
|
529
|
+
completed: result.completed,
|
|
530
|
+
answer: result.answer,
|
|
531
|
+
threads: threadManager.getThreads(),
|
|
532
|
+
budget: threadManager.getBudgetState(),
|
|
533
|
+
cacheStats: threadManager.getCacheStats(),
|
|
534
|
+
episodeCount: episodicMemory?.size,
|
|
535
|
+
};
|
|
536
|
+
renderSummary(summary);
|
|
537
|
+
// Output the answer
|
|
538
|
+
if (isJsonMode()) {
|
|
539
|
+
// Already output via renderSummary
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
process.stderr.write("\n");
|
|
543
|
+
logAnswer(result.answer);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
spinner.stop();
|
|
548
|
+
dashboard.clear();
|
|
549
|
+
process.removeListener("SIGINT", abortAndExit);
|
|
550
|
+
process.removeListener("SIGTERM", abortAndExit);
|
|
551
|
+
repl.shutdown();
|
|
552
|
+
await threadManager.cleanup();
|
|
553
|
+
// Shut down any managed OpenCode server instances
|
|
554
|
+
await opencodeMod.disableServerMode();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
//# sourceMappingURL=swarm.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread cache — caches thread results by (task, files, agent) hash.
|
|
3
|
+
*
|
|
4
|
+
* Slate's "subthread reuse" optimization: when the orchestrator spawns
|
|
5
|
+
* an identical thread (same task + same files + same agent), return the
|
|
6
|
+
* cached result instead of re-running the agent. Saves cost and time.
|
|
7
|
+
*
|
|
8
|
+
* Two modes:
|
|
9
|
+
* 1. Session-scoped (default): in-memory Map, cleared on exit.
|
|
10
|
+
* 2. Disk-persistent: reads/writes JSON files in a cache directory,
|
|
11
|
+
* with TTL-based expiry so stale entries don't accumulate.
|
|
12
|
+
*
|
|
13
|
+
* Cache keys are SHA-256 hashes of normalized (task, files, agent, model).
|
|
14
|
+
*/
|
|
15
|
+
import type { CompressedResult } from "../core/types.js";
|
|
16
|
+
export interface ThreadCacheEntry {
|
|
17
|
+
result: CompressedResult;
|
|
18
|
+
cachedAt: number;
|
|
19
|
+
hitCount: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ThreadCacheStats {
|
|
22
|
+
size: number;
|
|
23
|
+
hits: number;
|
|
24
|
+
misses: number;
|
|
25
|
+
totalSavedMs: number;
|
|
26
|
+
totalSavedUsd: number;
|
|
27
|
+
persistedEntries: number;
|
|
28
|
+
}
|
|
29
|
+
export declare class ThreadCache {
|
|
30
|
+
private cache;
|
|
31
|
+
private hits;
|
|
32
|
+
private misses;
|
|
33
|
+
private totalSavedMs;
|
|
34
|
+
private totalSavedUsd;
|
|
35
|
+
private maxEntries;
|
|
36
|
+
private persistDir?;
|
|
37
|
+
private ttlMs;
|
|
38
|
+
private persistedKeys;
|
|
39
|
+
constructor(maxEntries?: number, persistDir?: string, ttlHours?: number);
|
|
40
|
+
/** Initialize persistent cache — load entries from disk. */
|
|
41
|
+
init(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Look up a cached result for the given thread parameters.
|
|
44
|
+
* Returns undefined on cache miss.
|
|
45
|
+
*/
|
|
46
|
+
get(task: string, files: string[], agent: string, model: string): CompressedResult | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Store a thread result in the cache.
|
|
49
|
+
* Only caches successful results — failed threads should be retried.
|
|
50
|
+
*/
|
|
51
|
+
set(task: string, files: string[], agent: string, model: string, result: CompressedResult): void;
|
|
52
|
+
/** Get cache statistics. */
|
|
53
|
+
getStats(): ThreadCacheStats;
|
|
54
|
+
/** Clear all cached entries (in-memory and on disk). */
|
|
55
|
+
clear(): void;
|
|
56
|
+
private saveDiskEntry;
|
|
57
|
+
private deleteDiskEntry;
|
|
58
|
+
}
|