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,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool definitions and handlers for swarm-code.
|
|
3
|
+
*
|
|
4
|
+
* Tools:
|
|
5
|
+
* - swarm_run: Full orchestrated swarm execution (subprocess)
|
|
6
|
+
* - swarm_thread: Spawn a single coding agent thread in a worktree
|
|
7
|
+
* - swarm_status: Get current session status (threads, budget)
|
|
8
|
+
* - swarm_merge: Merge completed thread branches
|
|
9
|
+
* - swarm_cancel: Cancel running thread(s)
|
|
10
|
+
* - swarm_cleanup: Destroy session and worktrees
|
|
11
|
+
*/
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { cancelThreads, cleanupSession, getBudgetState, getSession, getThreads, mergeThreads, spawnThread, } from "./session.js";
|
|
18
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
19
|
+
function _textResult(text) {
|
|
20
|
+
return { content: [{ type: "text", text }] };
|
|
21
|
+
}
|
|
22
|
+
function errorResult(text) {
|
|
23
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
24
|
+
}
|
|
25
|
+
function jsonResult(data) {
|
|
26
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
27
|
+
}
|
|
28
|
+
/** Track active subprocesses so they can be killed on shutdown. */
|
|
29
|
+
const activeSubprocesses = new Set();
|
|
30
|
+
/** Kill all tracked subprocesses. Called during server shutdown. */
|
|
31
|
+
export function killActiveSubprocesses() {
|
|
32
|
+
for (const child of activeSubprocesses) {
|
|
33
|
+
try {
|
|
34
|
+
child.kill("SIGTERM");
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* already dead */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
activeSubprocesses.clear();
|
|
41
|
+
}
|
|
42
|
+
// ── Tool Registration ──────────────────────────────────────────────────────
|
|
43
|
+
export function registerTools(server, defaultDir) {
|
|
44
|
+
// Helper: resolve dir from tool args or default, validate existence
|
|
45
|
+
function resolveDir(dir) {
|
|
46
|
+
const resolved = dir || defaultDir;
|
|
47
|
+
if (!resolved)
|
|
48
|
+
return null;
|
|
49
|
+
const abs = resolve(resolved);
|
|
50
|
+
if (!existsSync(abs))
|
|
51
|
+
return null;
|
|
52
|
+
return abs;
|
|
53
|
+
}
|
|
54
|
+
// ── swarm_run ──────────────────────────────────────────────────────────
|
|
55
|
+
// Full swarm orchestration — runs the entire RLM loop as a subprocess.
|
|
56
|
+
// This is the high-level "do everything" tool.
|
|
57
|
+
server.registerTool("swarm_run", {
|
|
58
|
+
title: "Run Swarm",
|
|
59
|
+
description: "Run the full swarm orchestrator on a repository. Decomposes a task into " +
|
|
60
|
+
"parallel coding agent threads, executes them in isolated git worktrees, " +
|
|
61
|
+
"and merges results. Returns a JSON summary with success status, answer, " +
|
|
62
|
+
"thread stats, and cost breakdown.",
|
|
63
|
+
inputSchema: z.object({
|
|
64
|
+
dir: z.string().optional().describe("Path to the git repository (uses server default if not specified)"),
|
|
65
|
+
task: z.string().describe("The coding task to accomplish (e.g., 'add error handling to all API routes')"),
|
|
66
|
+
agent: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("Agent backend: opencode (default), claude-code, codex, aider, direct-llm"),
|
|
70
|
+
model: z.string().optional().describe("Orchestrator model override (e.g., claude-sonnet-4-6, gpt-4o)"),
|
|
71
|
+
max_budget: z
|
|
72
|
+
.number()
|
|
73
|
+
.min(0)
|
|
74
|
+
.max(50)
|
|
75
|
+
.optional()
|
|
76
|
+
.describe("Maximum budget in USD (default: 5.00, hard cap: 50.00)"),
|
|
77
|
+
auto_route: z.boolean().optional().describe("Enable auto model/agent routing per thread (default: false)"),
|
|
78
|
+
}),
|
|
79
|
+
}, async (args) => {
|
|
80
|
+
const resolvedDir = resolveDir(args.dir);
|
|
81
|
+
if (!resolvedDir) {
|
|
82
|
+
if (!args.dir && !defaultDir)
|
|
83
|
+
return errorResult("'dir' is required — specify the repo path");
|
|
84
|
+
return errorResult(`Directory does not exist: ${resolve(args.dir || defaultDir || "")}`);
|
|
85
|
+
}
|
|
86
|
+
// Build CLI args
|
|
87
|
+
const cliArgs = ["--dir", resolvedDir, "--json", "--quiet"];
|
|
88
|
+
if (args.agent)
|
|
89
|
+
cliArgs.push("--agent", args.agent);
|
|
90
|
+
if (args.model)
|
|
91
|
+
cliArgs.push("--orchestrator", args.model);
|
|
92
|
+
if (args.max_budget != null)
|
|
93
|
+
cliArgs.push("--max-budget", String(args.max_budget));
|
|
94
|
+
if (args.auto_route)
|
|
95
|
+
cliArgs.push("--auto-route");
|
|
96
|
+
cliArgs.push(args.task);
|
|
97
|
+
// Run swarm as subprocess
|
|
98
|
+
try {
|
|
99
|
+
const result = await runSwarmSubprocess(cliArgs);
|
|
100
|
+
return jsonResult(result);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
104
|
+
return errorResult(`Swarm execution failed: ${msg}`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// ── swarm_thread ──────────────────────────────────────────────────────
|
|
108
|
+
// Spawn a single coding agent thread — the low-level building block.
|
|
109
|
+
// The calling agent (Claude Code, Cursor) can orchestrate multiple threads.
|
|
110
|
+
server.registerTool("swarm_thread", {
|
|
111
|
+
title: "Spawn Thread",
|
|
112
|
+
description: "Spawn a single coding agent in an isolated git worktree. The agent " +
|
|
113
|
+
"executes the task, and the result (files changed, diff, summary) is " +
|
|
114
|
+
"returned. Use this for fine-grained control — call multiple times for " +
|
|
115
|
+
"parallel work, then use swarm_merge to integrate.",
|
|
116
|
+
inputSchema: z.object({
|
|
117
|
+
dir: z.string().optional().describe("Path to the git repository (uses server default if not specified)"),
|
|
118
|
+
task: z.string().describe("Task for the coding agent (e.g., 'fix the auth bug in src/auth.ts')"),
|
|
119
|
+
files: z.array(z.string()).optional().describe("File paths to focus on (hints for the agent)"),
|
|
120
|
+
agent: z
|
|
121
|
+
.string()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("Agent backend: opencode (default), claude-code, codex, aider, direct-llm"),
|
|
124
|
+
model: z.string().optional().describe("Model override (e.g., anthropic/claude-sonnet-4-6)"),
|
|
125
|
+
context: z.string().optional().describe("Additional context to pass to the agent"),
|
|
126
|
+
}),
|
|
127
|
+
}, async (args) => {
|
|
128
|
+
const dir = args.dir || defaultDir;
|
|
129
|
+
if (!dir)
|
|
130
|
+
return errorResult("'dir' is required — specify the repo path");
|
|
131
|
+
try {
|
|
132
|
+
const session = await getSession(dir);
|
|
133
|
+
const result = await spawnThread(session, {
|
|
134
|
+
task: args.task,
|
|
135
|
+
files: args.files,
|
|
136
|
+
agent: args.agent,
|
|
137
|
+
model: args.model,
|
|
138
|
+
context: args.context,
|
|
139
|
+
});
|
|
140
|
+
return jsonResult({
|
|
141
|
+
success: result.success,
|
|
142
|
+
summary: result.summary,
|
|
143
|
+
files_changed: result.filesChanged,
|
|
144
|
+
diff_stats: result.diffStats,
|
|
145
|
+
duration_ms: result.durationMs,
|
|
146
|
+
cost_usd: result.estimatedCostUsd,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
return errorResult(`Thread failed: ${msg}`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// ── swarm_status ──────────────────────────────────────────────────────
|
|
155
|
+
// Get status of all threads and budget for a session.
|
|
156
|
+
server.registerTool("swarm_status", {
|
|
157
|
+
title: "Session Status",
|
|
158
|
+
description: "Get the current status of a swarm session — all threads with their " +
|
|
159
|
+
"status (pending/running/completed/failed), budget spent, and cost breakdown.",
|
|
160
|
+
inputSchema: z.object({
|
|
161
|
+
dir: z.string().optional().describe("Path to the git repository (uses server default if not specified)"),
|
|
162
|
+
}),
|
|
163
|
+
}, async (args) => {
|
|
164
|
+
const dir = args.dir || defaultDir;
|
|
165
|
+
if (!dir)
|
|
166
|
+
return errorResult("'dir' is required — specify the repo path");
|
|
167
|
+
try {
|
|
168
|
+
const session = await getSession(dir);
|
|
169
|
+
const threads = getThreads(session);
|
|
170
|
+
const budget = getBudgetState(session);
|
|
171
|
+
const threadSummaries = threads.map((t) => ({
|
|
172
|
+
id: t.id,
|
|
173
|
+
task: t.config.task,
|
|
174
|
+
status: t.status,
|
|
175
|
+
phase: t.phase,
|
|
176
|
+
agent: t.config.agent.backend,
|
|
177
|
+
model: t.config.agent.model,
|
|
178
|
+
files_changed: t.result?.filesChanged || [],
|
|
179
|
+
duration_ms: t.completedAt && t.startedAt ? t.completedAt - t.startedAt : t.startedAt ? Date.now() - t.startedAt : 0,
|
|
180
|
+
cost_usd: t.result?.estimatedCostUsd ?? t.estimatedCostUsd,
|
|
181
|
+
error: t.error,
|
|
182
|
+
}));
|
|
183
|
+
return jsonResult({
|
|
184
|
+
dir: session.dir,
|
|
185
|
+
threads: threadSummaries,
|
|
186
|
+
counts: {
|
|
187
|
+
total: threads.length,
|
|
188
|
+
running: threads.filter((t) => t.status === "running").length,
|
|
189
|
+
completed: threads.filter((t) => t.status === "completed").length,
|
|
190
|
+
failed: threads.filter((t) => t.status === "failed").length,
|
|
191
|
+
pending: threads.filter((t) => t.status === "pending").length,
|
|
192
|
+
},
|
|
193
|
+
budget: {
|
|
194
|
+
spent_usd: budget.totalSpentUsd,
|
|
195
|
+
limit_usd: budget.sessionLimitUsd,
|
|
196
|
+
per_thread_limit_usd: budget.perThreadLimitUsd,
|
|
197
|
+
tokens: budget.totalTokens,
|
|
198
|
+
},
|
|
199
|
+
session_age_ms: Date.now() - session.createdAt,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
204
|
+
return errorResult(`Status check failed: ${msg}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
// ── swarm_merge ───────────────────────────────────────────────────────
|
|
208
|
+
// Merge completed thread branches back to main.
|
|
209
|
+
server.registerTool("swarm_merge", {
|
|
210
|
+
title: "Merge Threads",
|
|
211
|
+
description: "Merge all completed thread branches back into the main branch. " +
|
|
212
|
+
"Threads that are still running or failed are skipped. Returns " +
|
|
213
|
+
"per-branch merge results including any conflicts.",
|
|
214
|
+
inputSchema: z.object({
|
|
215
|
+
dir: z.string().optional().describe("Path to the git repository (uses server default if not specified)"),
|
|
216
|
+
}),
|
|
217
|
+
}, async (args) => {
|
|
218
|
+
const dir = args.dir || defaultDir;
|
|
219
|
+
if (!dir)
|
|
220
|
+
return errorResult("'dir' is required — specify the repo path");
|
|
221
|
+
try {
|
|
222
|
+
const session = await getSession(dir);
|
|
223
|
+
const results = await mergeThreads(session);
|
|
224
|
+
return jsonResult({
|
|
225
|
+
merged: results.length,
|
|
226
|
+
results: results.map((r) => ({
|
|
227
|
+
branch: r.branch,
|
|
228
|
+
success: r.success,
|
|
229
|
+
message: r.message,
|
|
230
|
+
conflicts: r.conflicts,
|
|
231
|
+
})),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
236
|
+
return errorResult(`Merge failed: ${msg}`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// ── swarm_cancel ──────────────────────────────────────────────────────
|
|
240
|
+
// Cancel a specific thread or all threads.
|
|
241
|
+
server.registerTool("swarm_cancel", {
|
|
242
|
+
title: "Cancel Threads",
|
|
243
|
+
description: "Cancel running threads. Specify a thread_id to cancel a specific " +
|
|
244
|
+
"thread, or omit to cancel all running threads in the session.",
|
|
245
|
+
inputSchema: z.object({
|
|
246
|
+
dir: z.string().optional().describe("Path to the git repository (uses server default if not specified)"),
|
|
247
|
+
thread_id: z.string().optional().describe("Specific thread ID to cancel (omit to cancel all)"),
|
|
248
|
+
}),
|
|
249
|
+
}, async (args) => {
|
|
250
|
+
const dir = args.dir || defaultDir;
|
|
251
|
+
if (!dir)
|
|
252
|
+
return errorResult("'dir' is required — specify the repo path");
|
|
253
|
+
try {
|
|
254
|
+
const session = await getSession(dir);
|
|
255
|
+
const result = cancelThreads(session, args.thread_id);
|
|
256
|
+
return jsonResult(result);
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
260
|
+
return errorResult(`Cancel failed: ${msg}`);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
// ── swarm_cleanup ─────────────────────────────────────────────────────
|
|
264
|
+
// Destroy a session — cancels threads, removes worktrees.
|
|
265
|
+
server.registerTool("swarm_cleanup", {
|
|
266
|
+
title: "Cleanup Session",
|
|
267
|
+
description: "Clean up a swarm session — cancels all running threads, removes " +
|
|
268
|
+
"worktrees, and frees resources. Call this when you're done with a " +
|
|
269
|
+
"directory to avoid leftover worktrees.",
|
|
270
|
+
inputSchema: z.object({
|
|
271
|
+
dir: z.string().optional().describe("Path to the git repository (uses server default if not specified)"),
|
|
272
|
+
}),
|
|
273
|
+
}, async (args) => {
|
|
274
|
+
const dir = args.dir || defaultDir;
|
|
275
|
+
if (!dir)
|
|
276
|
+
return errorResult("'dir' is required — specify the repo path");
|
|
277
|
+
try {
|
|
278
|
+
const message = await cleanupSession(dir);
|
|
279
|
+
return jsonResult({ cleaned_up: true, message });
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
283
|
+
return errorResult(`Cleanup failed: ${msg}`);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// ── Subprocess runner ──────────────────────────────────────────────────────
|
|
288
|
+
/**
|
|
289
|
+
* Run swarm as a subprocess with --json --quiet flags.
|
|
290
|
+
* Tracks the child process so it can be killed on server shutdown.
|
|
291
|
+
*/
|
|
292
|
+
function runSwarmSubprocess(args) {
|
|
293
|
+
return new Promise((resolve, reject) => {
|
|
294
|
+
const { bin, binArgs } = findSwarmEntrypoint();
|
|
295
|
+
const child = execFile(bin, [...binArgs, ...args], {
|
|
296
|
+
encoding: "utf-8",
|
|
297
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
298
|
+
timeout: 30 * 60 * 1000,
|
|
299
|
+
env: { ...process.env },
|
|
300
|
+
}, (err, stdout, stderr) => {
|
|
301
|
+
activeSubprocesses.delete(child);
|
|
302
|
+
if (err) {
|
|
303
|
+
// Try to parse JSON from stdout even on error
|
|
304
|
+
const parsed = tryParseSwarmJson(stdout);
|
|
305
|
+
if (parsed) {
|
|
306
|
+
resolve(parsed);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
reject(new Error(stderr || err.message));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const parsed = tryParseSwarmJson(stdout);
|
|
313
|
+
if (parsed) {
|
|
314
|
+
resolve(parsed);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
reject(new Error(`Could not parse swarm output: ${stdout.slice(0, 500)}`));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
activeSubprocesses.add(child);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Find the swarm entrypoint — prefers compiled dist/ over source.
|
|
325
|
+
* Returns the binary and args needed to run it.
|
|
326
|
+
*/
|
|
327
|
+
function findSwarmEntrypoint() {
|
|
328
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
329
|
+
const root = join(__dir, "..", "..");
|
|
330
|
+
const distEntry = join(root, "dist", "main.js");
|
|
331
|
+
if (existsSync(distEntry)) {
|
|
332
|
+
return { bin: "node", binArgs: [distEntry] };
|
|
333
|
+
}
|
|
334
|
+
// Fallback: use npx tsx to run TypeScript source
|
|
335
|
+
const srcEntry = join(root, "src", "main.ts");
|
|
336
|
+
if (existsSync(srcEntry)) {
|
|
337
|
+
return { bin: "npx", binArgs: ["tsx", srcEntry] };
|
|
338
|
+
}
|
|
339
|
+
throw new Error("swarm-code entrypoint not found. Run 'npm run build' or install swarm-code globally.");
|
|
340
|
+
}
|
|
341
|
+
function tryParseSwarmJson(stdout) {
|
|
342
|
+
if (!stdout)
|
|
343
|
+
return null;
|
|
344
|
+
const trimmed = stdout.trim();
|
|
345
|
+
// Try single-line JSON (last line that starts with {)
|
|
346
|
+
const lines = trimmed.split("\n");
|
|
347
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
348
|
+
const line = lines[i].trim();
|
|
349
|
+
if (line.startsWith("{")) {
|
|
350
|
+
try {
|
|
351
|
+
const parsed = JSON.parse(line);
|
|
352
|
+
if (typeof parsed === "object" && parsed !== null && "success" in parsed) {
|
|
353
|
+
return parsed;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
/* not JSON */
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Fallback: first { to last }
|
|
362
|
+
const firstBrace = trimmed.indexOf("{");
|
|
363
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
364
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
365
|
+
try {
|
|
366
|
+
const parsed = JSON.parse(trimmed.slice(firstBrace, lastBrace + 1));
|
|
367
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
368
|
+
return parsed;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
/* give up */
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Episodic memory — persists successful thread strategies to disk.
|
|
3
|
+
*
|
|
4
|
+
* Records episodes after successful thread completions:
|
|
5
|
+
* - Task pattern (normalized keywords)
|
|
6
|
+
* - Agent + model used
|
|
7
|
+
* - Result quality (success, duration, cost, files changed)
|
|
8
|
+
* - Task slot + complexity classification
|
|
9
|
+
*
|
|
10
|
+
* Recall: given a new task, find similar past episodes and return
|
|
11
|
+
* the strategies that worked. Used to inform agent/model selection
|
|
12
|
+
* and provide context hints to the orchestrator.
|
|
13
|
+
*
|
|
14
|
+
* Storage: JSON files in ~/.swarm/memory/episodes/ indexed by
|
|
15
|
+
* task similarity hash (trigram-based).
|
|
16
|
+
*/
|
|
17
|
+
export interface Episode {
|
|
18
|
+
/** Unique episode ID (SHA-256 of task + timestamp). */
|
|
19
|
+
id: string;
|
|
20
|
+
/** Original task description. */
|
|
21
|
+
task: string;
|
|
22
|
+
/** Normalized task keywords for similarity matching. */
|
|
23
|
+
taskKeywords: string[];
|
|
24
|
+
/** Agent backend used. */
|
|
25
|
+
agent: string;
|
|
26
|
+
/** Model used. */
|
|
27
|
+
model: string;
|
|
28
|
+
/** Task slot (execution/search/reasoning/planning). */
|
|
29
|
+
slot: string;
|
|
30
|
+
/** Task complexity (simple/medium/complex). */
|
|
31
|
+
complexity: string;
|
|
32
|
+
/** Whether the thread succeeded. */
|
|
33
|
+
success: boolean;
|
|
34
|
+
/** Duration in ms. */
|
|
35
|
+
durationMs: number;
|
|
36
|
+
/** Estimated cost in USD. */
|
|
37
|
+
estimatedCostUsd: number;
|
|
38
|
+
/** Number of files changed. */
|
|
39
|
+
filesChangedCount: number;
|
|
40
|
+
/** File paths changed (for pattern matching). */
|
|
41
|
+
filesChanged: string[];
|
|
42
|
+
/** Compressed result summary (the episode content). */
|
|
43
|
+
summary: string;
|
|
44
|
+
/** Timestamp. */
|
|
45
|
+
timestamp: number;
|
|
46
|
+
}
|
|
47
|
+
export interface EpisodeRecall {
|
|
48
|
+
episode: Episode;
|
|
49
|
+
/** Similarity score (0-1). */
|
|
50
|
+
similarity: number;
|
|
51
|
+
}
|
|
52
|
+
/** Per-agent aggregate statistics derived from episodic memory. */
|
|
53
|
+
export interface AgentAggregateStats {
|
|
54
|
+
/** Total number of successful episodes. */
|
|
55
|
+
totalEpisodes: number;
|
|
56
|
+
/** Average duration in ms. */
|
|
57
|
+
avgDurationMs: number;
|
|
58
|
+
/** Average cost in USD. */
|
|
59
|
+
avgCostUsd: number;
|
|
60
|
+
/** Success count per slot (execution/search/reasoning/planning). */
|
|
61
|
+
slotCounts: Map<string, number>;
|
|
62
|
+
/** File extensions this agent has successfully worked with. */
|
|
63
|
+
fileExtensions: Set<string>;
|
|
64
|
+
}
|
|
65
|
+
/** Aggregate stats across all agents. */
|
|
66
|
+
export interface AggregateStats {
|
|
67
|
+
/** Per-agent statistics. */
|
|
68
|
+
perAgent: Map<string, AgentAggregateStats>;
|
|
69
|
+
/**
|
|
70
|
+
* File extension → agent → success count.
|
|
71
|
+
* Used by the router to boost agents that handle specific file types well.
|
|
72
|
+
*/
|
|
73
|
+
fileExtensions: Map<string, Map<string, number>>;
|
|
74
|
+
}
|
|
75
|
+
export declare class EpisodicMemory {
|
|
76
|
+
private memoryDir;
|
|
77
|
+
private episodes;
|
|
78
|
+
private loaded;
|
|
79
|
+
private maxEpisodes;
|
|
80
|
+
constructor(memoryDir: string, maxEpisodes?: number);
|
|
81
|
+
/** Initialize — create directory and load existing episodes. */
|
|
82
|
+
init(): Promise<void>;
|
|
83
|
+
/** Record a new episode from a completed thread. */
|
|
84
|
+
record(params: {
|
|
85
|
+
task: string;
|
|
86
|
+
agent: string;
|
|
87
|
+
model: string;
|
|
88
|
+
slot: string;
|
|
89
|
+
complexity: string;
|
|
90
|
+
success: boolean;
|
|
91
|
+
durationMs: number;
|
|
92
|
+
estimatedCostUsd: number;
|
|
93
|
+
filesChanged: string[];
|
|
94
|
+
summary: string;
|
|
95
|
+
}): Promise<Episode>;
|
|
96
|
+
/**
|
|
97
|
+
* Recall similar past episodes for a given task.
|
|
98
|
+
* Returns episodes sorted by similarity (highest first).
|
|
99
|
+
*/
|
|
100
|
+
recall(task: string, maxResults?: number, minSimilarity?: number): EpisodeRecall[];
|
|
101
|
+
/**
|
|
102
|
+
* Get strategy recommendations based on past episodes.
|
|
103
|
+
* Returns a formatted string for inclusion in orchestrator context.
|
|
104
|
+
*/
|
|
105
|
+
getStrategyHints(task: string): string | null;
|
|
106
|
+
/**
|
|
107
|
+
* Get the best agent/model recommendation for a task based on past episodes.
|
|
108
|
+
* Returns null if no relevant episodes found.
|
|
109
|
+
*/
|
|
110
|
+
recommendStrategy(task: string): {
|
|
111
|
+
agent: string;
|
|
112
|
+
model: string;
|
|
113
|
+
confidence: number;
|
|
114
|
+
} | null;
|
|
115
|
+
/**
|
|
116
|
+
* Get aggregate statistics across all episodes, grouped by agent.
|
|
117
|
+
*
|
|
118
|
+
* Returns per-agent success rates, average costs/durations, slot distributions,
|
|
119
|
+
* and a file-extension-to-agent mapping. Used by the model router as a fallback
|
|
120
|
+
* when no high-confidence episodic match exists.
|
|
121
|
+
*
|
|
122
|
+
* Returns null if no episodes are loaded (graceful degradation).
|
|
123
|
+
*/
|
|
124
|
+
getAggregateStats(): AggregateStats | null;
|
|
125
|
+
/** Get total episode count. */
|
|
126
|
+
get size(): number;
|
|
127
|
+
/** Get all episodes (for viewer). */
|
|
128
|
+
getAll(): Episode[];
|
|
129
|
+
private loadAll;
|
|
130
|
+
private saveEpisode;
|
|
131
|
+
private deleteFile;
|
|
132
|
+
}
|