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
|
@@ -0,0 +1,198 @@
|
|
|
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 { createHash } from "node:crypto";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
/**
|
|
19
|
+
* Compute a stable cache key from thread parameters.
|
|
20
|
+
* Normalizes inputs: trims task, sorts files, lowercases agent/model.
|
|
21
|
+
*/
|
|
22
|
+
function computeCacheKey(task, files, agent, model) {
|
|
23
|
+
const normalized = JSON.stringify({
|
|
24
|
+
task: task.trim(),
|
|
25
|
+
files: [...files].sort(),
|
|
26
|
+
agent: agent.toLowerCase(),
|
|
27
|
+
model: model.toLowerCase(),
|
|
28
|
+
});
|
|
29
|
+
return createHash("sha256").update(normalized).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
export class ThreadCache {
|
|
32
|
+
cache = new Map();
|
|
33
|
+
hits = 0;
|
|
34
|
+
misses = 0;
|
|
35
|
+
totalSavedMs = 0;
|
|
36
|
+
totalSavedUsd = 0;
|
|
37
|
+
maxEntries;
|
|
38
|
+
persistDir;
|
|
39
|
+
ttlMs;
|
|
40
|
+
persistedKeys = new Set();
|
|
41
|
+
constructor(maxEntries = 100, persistDir, ttlHours = 24) {
|
|
42
|
+
this.maxEntries = maxEntries;
|
|
43
|
+
this.persistDir = persistDir;
|
|
44
|
+
this.ttlMs = ttlHours * 60 * 60 * 1000;
|
|
45
|
+
}
|
|
46
|
+
/** Initialize persistent cache — load entries from disk. */
|
|
47
|
+
async init() {
|
|
48
|
+
if (!this.persistDir)
|
|
49
|
+
return;
|
|
50
|
+
fs.mkdirSync(this.persistDir, { recursive: true });
|
|
51
|
+
let files;
|
|
52
|
+
try {
|
|
53
|
+
files = fs.readdirSync(this.persistDir).filter((f) => f.endsWith(".json"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
try {
|
|
61
|
+
const filePath = path.join(this.persistDir, file);
|
|
62
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
63
|
+
const entry = JSON.parse(raw);
|
|
64
|
+
// Skip expired entries
|
|
65
|
+
if (now - entry.cachedAt > this.ttlMs) {
|
|
66
|
+
fs.unlinkSync(filePath);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Only load successful results
|
|
70
|
+
if (!entry.result.success) {
|
|
71
|
+
fs.unlinkSync(filePath);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
this.cache.set(entry.key, {
|
|
75
|
+
result: entry.result,
|
|
76
|
+
cachedAt: entry.cachedAt,
|
|
77
|
+
hitCount: 0,
|
|
78
|
+
});
|
|
79
|
+
this.persistedKeys.add(entry.key);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Skip corrupt files
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Trim to max capacity
|
|
86
|
+
while (this.cache.size > this.maxEntries) {
|
|
87
|
+
const oldestKey = this.cache.keys().next().value;
|
|
88
|
+
if (oldestKey !== undefined) {
|
|
89
|
+
this.cache.delete(oldestKey);
|
|
90
|
+
this.deleteDiskEntry(oldestKey);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Look up a cached result for the given thread parameters.
|
|
96
|
+
* Returns undefined on cache miss.
|
|
97
|
+
*/
|
|
98
|
+
get(task, files, agent, model) {
|
|
99
|
+
const key = computeCacheKey(task, files, agent, model);
|
|
100
|
+
const entry = this.cache.get(key);
|
|
101
|
+
if (!entry) {
|
|
102
|
+
this.misses++;
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
// Check TTL expiry
|
|
106
|
+
if (Date.now() - entry.cachedAt > this.ttlMs) {
|
|
107
|
+
this.cache.delete(key);
|
|
108
|
+
this.deleteDiskEntry(key);
|
|
109
|
+
this.misses++;
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
// Only return successful cached results
|
|
113
|
+
if (!entry.result.success) {
|
|
114
|
+
this.misses++;
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
this.hits++;
|
|
118
|
+
entry.hitCount++;
|
|
119
|
+
this.totalSavedMs += entry.result.durationMs;
|
|
120
|
+
this.totalSavedUsd += entry.result.estimatedCostUsd;
|
|
121
|
+
return entry.result;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Store a thread result in the cache.
|
|
125
|
+
* Only caches successful results — failed threads should be retried.
|
|
126
|
+
*/
|
|
127
|
+
set(task, files, agent, model, result) {
|
|
128
|
+
// Don't cache failures
|
|
129
|
+
if (!result.success)
|
|
130
|
+
return;
|
|
131
|
+
// Evict oldest entry if at capacity
|
|
132
|
+
if (this.cache.size >= this.maxEntries) {
|
|
133
|
+
const oldestKey = this.cache.keys().next().value;
|
|
134
|
+
if (oldestKey !== undefined) {
|
|
135
|
+
this.cache.delete(oldestKey);
|
|
136
|
+
this.deleteDiskEntry(oldestKey);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const key = computeCacheKey(task, files, agent, model);
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
this.cache.set(key, {
|
|
142
|
+
result,
|
|
143
|
+
cachedAt: now,
|
|
144
|
+
hitCount: 0,
|
|
145
|
+
});
|
|
146
|
+
// Persist to disk
|
|
147
|
+
this.saveDiskEntry(key, result, now);
|
|
148
|
+
}
|
|
149
|
+
/** Get cache statistics. */
|
|
150
|
+
getStats() {
|
|
151
|
+
return {
|
|
152
|
+
size: this.cache.size,
|
|
153
|
+
hits: this.hits,
|
|
154
|
+
misses: this.misses,
|
|
155
|
+
totalSavedMs: this.totalSavedMs,
|
|
156
|
+
totalSavedUsd: this.totalSavedUsd,
|
|
157
|
+
persistedEntries: this.persistedKeys.size,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** Clear all cached entries (in-memory and on disk). */
|
|
161
|
+
clear() {
|
|
162
|
+
if (this.persistDir) {
|
|
163
|
+
for (const key of this.cache.keys()) {
|
|
164
|
+
this.deleteDiskEntry(key);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.cache.clear();
|
|
168
|
+
}
|
|
169
|
+
// ── Disk persistence ──────────────────────────────────────────────────
|
|
170
|
+
saveDiskEntry(key, result, cachedAt) {
|
|
171
|
+
if (!this.persistDir)
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
const entry = { key, result, cachedAt };
|
|
175
|
+
const filePath = path.join(this.persistDir, `${key}.json`);
|
|
176
|
+
fs.writeFileSync(filePath, JSON.stringify(entry), "utf-8");
|
|
177
|
+
this.persistedKeys.add(key);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Non-fatal
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
deleteDiskEntry(key) {
|
|
184
|
+
if (!this.persistDir)
|
|
185
|
+
return;
|
|
186
|
+
try {
|
|
187
|
+
const filePath = path.join(this.persistDir, `${key}.json`);
|
|
188
|
+
if (fs.existsSync(filePath)) {
|
|
189
|
+
fs.unlinkSync(filePath);
|
|
190
|
+
this.persistedKeys.delete(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Non-fatal
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread manager — spawns and manages coding agent threads in isolated worktrees.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - AsyncSemaphore for proper concurrency gating (no polling)
|
|
6
|
+
* - AbortSignal propagation for thread cancellation
|
|
7
|
+
* - Per-thread retry logic with exponential backoff
|
|
8
|
+
* - Error classification: retryable (transient) vs fatal (permanent)
|
|
9
|
+
* - Agent/model re-routing on failure (fallback to alternative combos)
|
|
10
|
+
* - Budget tracking and enforcement
|
|
11
|
+
* - Per-thread error isolation
|
|
12
|
+
*/
|
|
13
|
+
import type { BudgetState, CompressedResult, SwarmConfig, ThreadConfig, ThreadProgressPhase, ThreadState } from "../core/types.js";
|
|
14
|
+
import type { EpisodicMemory } from "../memory/episodic.js";
|
|
15
|
+
import { WorktreeManager } from "../worktree/manager.js";
|
|
16
|
+
import { type ThreadCacheStats } from "./cache.js";
|
|
17
|
+
/**
|
|
18
|
+
* Promise-based semaphore for concurrency control.
|
|
19
|
+
* acquire() resolves when a slot is available, release() frees a slot.
|
|
20
|
+
*/
|
|
21
|
+
export declare class AsyncSemaphore {
|
|
22
|
+
private current;
|
|
23
|
+
private readonly max;
|
|
24
|
+
private waiters;
|
|
25
|
+
constructor(max: number);
|
|
26
|
+
acquire(): Promise<void>;
|
|
27
|
+
release(): void;
|
|
28
|
+
get activeCount(): number;
|
|
29
|
+
get waitingCount(): number;
|
|
30
|
+
}
|
|
31
|
+
export type ThreadProgressCallback = (threadId: string, phase: ThreadProgressPhase, detail?: string) => void;
|
|
32
|
+
export declare class ThreadManager {
|
|
33
|
+
private threads;
|
|
34
|
+
private totalSpawned;
|
|
35
|
+
private semaphore;
|
|
36
|
+
private worktreeManager;
|
|
37
|
+
private config;
|
|
38
|
+
private budget;
|
|
39
|
+
private threadCache;
|
|
40
|
+
private episodicMemory?;
|
|
41
|
+
private onThreadProgress?;
|
|
42
|
+
private sessionAbort?;
|
|
43
|
+
private threadAbortControllers;
|
|
44
|
+
constructor(repoRoot: string, config: SwarmConfig, onThreadProgress?: ThreadProgressCallback, sessionAbort?: AbortSignal);
|
|
45
|
+
/** Set the episodic memory store for recording thread outcomes. */
|
|
46
|
+
setEpisodicMemory(memory: EpisodicMemory): void;
|
|
47
|
+
init(): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Spawn a thread — creates a worktree, runs the agent, returns compressed result.
|
|
50
|
+
* Checks the subthread cache first; on cache hit, returns immediately (Slate-style reuse).
|
|
51
|
+
* Retries up to config.thread_retries times on failure.
|
|
52
|
+
* Error-isolated: a failure here never throws — always returns a CompressedResult.
|
|
53
|
+
*/
|
|
54
|
+
spawnThread(threadConfig: ThreadConfig): Promise<CompressedResult>;
|
|
55
|
+
/**
|
|
56
|
+
* Execute a single thread attempt. Acquires semaphore, creates worktree,
|
|
57
|
+
* runs agent, captures diff, compresses result.
|
|
58
|
+
*/
|
|
59
|
+
private executeThread;
|
|
60
|
+
/** Cancel a specific running thread. */
|
|
61
|
+
cancelThread(threadId: string): boolean;
|
|
62
|
+
/** Cancel all running threads. */
|
|
63
|
+
cancelAll(): void;
|
|
64
|
+
/** Get all thread states. */
|
|
65
|
+
getThreads(): ThreadState[];
|
|
66
|
+
/** Get a specific thread's state. */
|
|
67
|
+
getThread(threadId: string): ThreadState | undefined;
|
|
68
|
+
/** Get the worktree manager for merge operations. */
|
|
69
|
+
getWorktreeManager(): WorktreeManager;
|
|
70
|
+
/** Get current budget state. */
|
|
71
|
+
getBudgetState(): BudgetState;
|
|
72
|
+
/** Get subthread cache stats. */
|
|
73
|
+
getCacheStats(): ThreadCacheStats;
|
|
74
|
+
/** Get concurrency stats. */
|
|
75
|
+
getConcurrencyStats(): {
|
|
76
|
+
active: number;
|
|
77
|
+
waiting: number;
|
|
78
|
+
total: number;
|
|
79
|
+
max: number;
|
|
80
|
+
};
|
|
81
|
+
/** Cleanup all worktrees. */
|
|
82
|
+
cleanup(): Promise<void>;
|
|
83
|
+
private cleanupWorktree;
|
|
84
|
+
private failResult;
|
|
85
|
+
}
|