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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/bin/swarm.mjs +45 -0
  4. package/dist/agents/aider.d.ts +12 -0
  5. package/dist/agents/aider.js +182 -0
  6. package/dist/agents/claude-code.d.ts +9 -0
  7. package/dist/agents/claude-code.js +216 -0
  8. package/dist/agents/codex.d.ts +14 -0
  9. package/dist/agents/codex.js +193 -0
  10. package/dist/agents/direct-llm.d.ts +9 -0
  11. package/dist/agents/direct-llm.js +78 -0
  12. package/dist/agents/mock.d.ts +9 -0
  13. package/dist/agents/mock.js +77 -0
  14. package/dist/agents/opencode.d.ts +23 -0
  15. package/dist/agents/opencode.js +571 -0
  16. package/dist/agents/provider.d.ts +11 -0
  17. package/dist/agents/provider.js +31 -0
  18. package/dist/cli.d.ts +15 -0
  19. package/dist/cli.js +285 -0
  20. package/dist/compression/compressor.d.ts +28 -0
  21. package/dist/compression/compressor.js +265 -0
  22. package/dist/config.d.ts +42 -0
  23. package/dist/config.js +170 -0
  24. package/dist/core/repl.d.ts +69 -0
  25. package/dist/core/repl.js +336 -0
  26. package/dist/core/rlm.d.ts +63 -0
  27. package/dist/core/rlm.js +409 -0
  28. package/dist/core/runtime.py +335 -0
  29. package/dist/core/types.d.ts +131 -0
  30. package/dist/core/types.js +19 -0
  31. package/dist/env.d.ts +10 -0
  32. package/dist/env.js +75 -0
  33. package/dist/interactive-swarm.d.ts +20 -0
  34. package/dist/interactive-swarm.js +1041 -0
  35. package/dist/interactive.d.ts +10 -0
  36. package/dist/interactive.js +1765 -0
  37. package/dist/main.d.ts +15 -0
  38. package/dist/main.js +242 -0
  39. package/dist/mcp/server.d.ts +15 -0
  40. package/dist/mcp/server.js +72 -0
  41. package/dist/mcp/session.d.ts +73 -0
  42. package/dist/mcp/session.js +184 -0
  43. package/dist/mcp/tools.d.ts +15 -0
  44. package/dist/mcp/tools.js +377 -0
  45. package/dist/memory/episodic.d.ts +132 -0
  46. package/dist/memory/episodic.js +390 -0
  47. package/dist/prompts/orchestrator.d.ts +5 -0
  48. package/dist/prompts/orchestrator.js +191 -0
  49. package/dist/routing/model-router.d.ts +130 -0
  50. package/dist/routing/model-router.js +515 -0
  51. package/dist/swarm.d.ts +14 -0
  52. package/dist/swarm.js +557 -0
  53. package/dist/threads/cache.d.ts +58 -0
  54. package/dist/threads/cache.js +198 -0
  55. package/dist/threads/manager.d.ts +85 -0
  56. package/dist/threads/manager.js +659 -0
  57. package/dist/ui/banner.d.ts +14 -0
  58. package/dist/ui/banner.js +42 -0
  59. package/dist/ui/dashboard.d.ts +33 -0
  60. package/dist/ui/dashboard.js +151 -0
  61. package/dist/ui/index.d.ts +10 -0
  62. package/dist/ui/index.js +11 -0
  63. package/dist/ui/log.d.ts +39 -0
  64. package/dist/ui/log.js +126 -0
  65. package/dist/ui/onboarding.d.ts +14 -0
  66. package/dist/ui/onboarding.js +518 -0
  67. package/dist/ui/spinner.d.ts +25 -0
  68. package/dist/ui/spinner.js +113 -0
  69. package/dist/ui/summary.d.ts +18 -0
  70. package/dist/ui/summary.js +113 -0
  71. package/dist/ui/theme.d.ts +63 -0
  72. package/dist/ui/theme.js +97 -0
  73. package/dist/viewer.d.ts +12 -0
  74. package/dist/viewer.js +1284 -0
  75. package/dist/worktree/manager.d.ts +45 -0
  76. package/dist/worktree/manager.js +266 -0
  77. package/dist/worktree/merge.d.ts +28 -0
  78. package/dist/worktree/merge.js +138 -0
  79. 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
+ }