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/cli.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Standalone RLM CLI — run Recursive Language Model queries from the terminal.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx tsx src/cli.ts --file large-file.txt "What are the main themes?"
|
|
7
|
+
* npx tsx src/cli.ts --url https://example.com/big.txt "Summarize this"
|
|
8
|
+
* cat data.txt | npx tsx src/cli.ts --stdin "Count the errors"
|
|
9
|
+
*
|
|
10
|
+
* Environment (pick one):
|
|
11
|
+
* ANTHROPIC_API_KEY — for Anthropic models
|
|
12
|
+
* OPENAI_API_KEY — for OpenAI models
|
|
13
|
+
* GEMINI_API_KEY — for Google models
|
|
14
|
+
*/
|
|
15
|
+
import "./env.js";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
// Dynamic imports — ensures env.js has set process.env BEFORE pi-ai loads
|
|
18
|
+
const { getModels, getProviders } = await import("@mariozechner/pi-ai");
|
|
19
|
+
const { PythonRepl } = await import("./core/repl.js");
|
|
20
|
+
const { runRlmLoop } = await import("./core/rlm.js");
|
|
21
|
+
// ── Arg parsing ─────────────────────────────────────────────────────────────
|
|
22
|
+
function usage() {
|
|
23
|
+
console.error(`
|
|
24
|
+
swarm run — Recursive Language Model CLI (arXiv:2512.24601)
|
|
25
|
+
|
|
26
|
+
USAGE
|
|
27
|
+
rlm run [OPTIONS] "<query>"
|
|
28
|
+
|
|
29
|
+
OPTIONS
|
|
30
|
+
--model <id> Model ID (default: RLM_MODEL from .env)
|
|
31
|
+
--file <path> Read context from a file
|
|
32
|
+
--url <url> Fetch context from a URL
|
|
33
|
+
--stdin Read context from stdin (pipe data in)
|
|
34
|
+
--verbose Show iteration progress
|
|
35
|
+
|
|
36
|
+
EXAMPLES
|
|
37
|
+
rlm run --file big.txt "List all classes"
|
|
38
|
+
curl -s https://example.com/large.py | rlm run --stdin "Summarize"
|
|
39
|
+
rlm run --url https://raw.githubusercontent.com/.../typing.py "Count public classes"
|
|
40
|
+
`.trim());
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
function parseArgs() {
|
|
44
|
+
const args = process.argv.slice(2);
|
|
45
|
+
let modelId;
|
|
46
|
+
let file;
|
|
47
|
+
let url;
|
|
48
|
+
let useStdin = false;
|
|
49
|
+
let verbose = false;
|
|
50
|
+
const positional = [];
|
|
51
|
+
for (let i = 0; i < args.length; i++) {
|
|
52
|
+
const arg = args[i];
|
|
53
|
+
if (arg === "--model" && i + 1 < args.length) {
|
|
54
|
+
modelId = args[++i];
|
|
55
|
+
}
|
|
56
|
+
else if (arg === "--file" && i + 1 < args.length) {
|
|
57
|
+
file = args[++i];
|
|
58
|
+
}
|
|
59
|
+
else if (arg === "--url" && i + 1 < args.length) {
|
|
60
|
+
url = args[++i];
|
|
61
|
+
}
|
|
62
|
+
else if (arg === "--stdin") {
|
|
63
|
+
useStdin = true;
|
|
64
|
+
}
|
|
65
|
+
else if (arg === "--verbose") {
|
|
66
|
+
verbose = true;
|
|
67
|
+
}
|
|
68
|
+
else if (arg === "--help" || arg === "-h") {
|
|
69
|
+
usage();
|
|
70
|
+
}
|
|
71
|
+
else if (!arg.startsWith("--")) {
|
|
72
|
+
positional.push(arg);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.error(`Unknown option: ${arg}`);
|
|
76
|
+
usage();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!modelId) {
|
|
80
|
+
modelId = process.env.RLM_MODEL || "claude-sonnet-4-6";
|
|
81
|
+
}
|
|
82
|
+
if (positional.length === 0) {
|
|
83
|
+
console.error("Error: query argument is required");
|
|
84
|
+
usage();
|
|
85
|
+
}
|
|
86
|
+
const query = positional.join(" ");
|
|
87
|
+
if (!file && !url && !useStdin) {
|
|
88
|
+
console.error("Error: one of --file, --url, or --stdin is required");
|
|
89
|
+
usage();
|
|
90
|
+
}
|
|
91
|
+
return { modelId, file, url, useStdin, verbose, query };
|
|
92
|
+
}
|
|
93
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
94
|
+
const MAX_STDIN_BYTES = 50 * 1024 * 1024; // 50MB
|
|
95
|
+
async function readStdin() {
|
|
96
|
+
const chunks = [];
|
|
97
|
+
let total = 0;
|
|
98
|
+
for await (const chunk of process.stdin) {
|
|
99
|
+
total += chunk.length;
|
|
100
|
+
if (total > MAX_STDIN_BYTES) {
|
|
101
|
+
throw new Error(`stdin exceeds ${MAX_STDIN_BYTES / 1024 / 1024}MB limit`);
|
|
102
|
+
}
|
|
103
|
+
chunks.push(chunk);
|
|
104
|
+
}
|
|
105
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
106
|
+
}
|
|
107
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
108
|
+
const MAX_RESPONSE_BYTES = 50 * 1024 * 1024; // 50MB
|
|
109
|
+
async function fetchUrl(url) {
|
|
110
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
111
|
+
if (!resp.ok) {
|
|
112
|
+
throw new Error(`Failed to fetch ${url}: ${resp.status} ${resp.statusText}`);
|
|
113
|
+
}
|
|
114
|
+
const contentLength = resp.headers.get("content-length");
|
|
115
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_BYTES) {
|
|
116
|
+
throw new Error(`Response too large (${(parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB). Limit is ${MAX_RESPONSE_BYTES / 1024 / 1024}MB.`);
|
|
117
|
+
}
|
|
118
|
+
const text = await resp.text();
|
|
119
|
+
if (text.length > MAX_RESPONSE_BYTES) {
|
|
120
|
+
throw new Error(`Response too large (${(text.length / 1024 / 1024).toFixed(1)}MB). Limit is ${MAX_RESPONSE_BYTES / 1024 / 1024}MB.`);
|
|
121
|
+
}
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
125
|
+
async function main() {
|
|
126
|
+
const args = parseArgs();
|
|
127
|
+
// Provider → env var mapping
|
|
128
|
+
const providerKeys = {
|
|
129
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
130
|
+
openai: "OPENAI_API_KEY",
|
|
131
|
+
google: "GEMINI_API_KEY",
|
|
132
|
+
};
|
|
133
|
+
const defaultModels = {
|
|
134
|
+
anthropic: "claude-sonnet-4-6",
|
|
135
|
+
openai: "gpt-4o",
|
|
136
|
+
google: "gemini-2.5-flash",
|
|
137
|
+
};
|
|
138
|
+
// Resolve model — ensure provider has an API key
|
|
139
|
+
// Prioritise well-known providers so e.g. "gpt-4o" picks "openai" not "azure-openai-responses"
|
|
140
|
+
let model;
|
|
141
|
+
let _resolvedProvider = "";
|
|
142
|
+
const allModelIds = [];
|
|
143
|
+
const knownProviders = new Set(Object.keys(providerKeys));
|
|
144
|
+
// First pass: only well-known providers
|
|
145
|
+
for (const provider of getProviders()) {
|
|
146
|
+
const providerModels = getModels(provider);
|
|
147
|
+
for (const m of providerModels) {
|
|
148
|
+
allModelIds.push(m.id);
|
|
149
|
+
if (!model && m.id === args.modelId && knownProviders.has(provider)) {
|
|
150
|
+
const key = providerKeys[provider];
|
|
151
|
+
if (process.env[key]) {
|
|
152
|
+
model = m;
|
|
153
|
+
_resolvedProvider = provider;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Second pass: remaining providers (if not found above)
|
|
159
|
+
if (!model) {
|
|
160
|
+
for (const provider of getProviders()) {
|
|
161
|
+
if (knownProviders.has(provider))
|
|
162
|
+
continue;
|
|
163
|
+
for (const m of getModels(provider)) {
|
|
164
|
+
if (m.id === args.modelId) {
|
|
165
|
+
const key = `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
166
|
+
if (process.env[key]) {
|
|
167
|
+
model = m;
|
|
168
|
+
_resolvedProvider = provider;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (model)
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Fallback: if default model's provider has no key, pick one that does
|
|
178
|
+
if (!model) {
|
|
179
|
+
for (const [prov, envKey] of Object.entries(providerKeys)) {
|
|
180
|
+
if (!process.env[envKey])
|
|
181
|
+
continue;
|
|
182
|
+
const fallbackId = defaultModels[prov];
|
|
183
|
+
if (!fallbackId)
|
|
184
|
+
continue;
|
|
185
|
+
for (const p of getProviders()) {
|
|
186
|
+
if (p !== prov)
|
|
187
|
+
continue;
|
|
188
|
+
for (const m of getModels(p)) {
|
|
189
|
+
if (m.id === fallbackId) {
|
|
190
|
+
model = m;
|
|
191
|
+
_resolvedProvider = prov;
|
|
192
|
+
args.modelId = fallbackId;
|
|
193
|
+
console.error(`Note: using ${fallbackId} (${prov}) — set RLM_MODEL to override`);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (model)
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
if (model)
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!model) {
|
|
205
|
+
console.error(`Error: unknown model "${args.modelId}"`);
|
|
206
|
+
console.error(`Available models: ${allModelIds.join(", ")}`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
// Load context
|
|
210
|
+
let context;
|
|
211
|
+
if (args.file) {
|
|
212
|
+
try {
|
|
213
|
+
const stat = fs.statSync(args.file);
|
|
214
|
+
if (stat.isDirectory()) {
|
|
215
|
+
console.error(`Error: "${args.file}" is a directory. Use the interactive mode (\`rlm\`) for directory loading.`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
console.error(`Error: could not access "${args.file}": ${err.message}`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
console.error(`Reading context from file: ${args.file}`);
|
|
224
|
+
try {
|
|
225
|
+
context = fs.readFileSync(args.file, "utf-8");
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
console.error(`Error: could not read file "${args.file}": ${err.message}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (args.url) {
|
|
233
|
+
console.error(`Fetching context from URL: ${args.url}`);
|
|
234
|
+
context = await fetchUrl(args.url);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
console.error("Reading context from stdin...");
|
|
238
|
+
context = await readStdin();
|
|
239
|
+
}
|
|
240
|
+
console.error(`Context loaded: ${context.length.toLocaleString()} characters`);
|
|
241
|
+
console.error(`Model: ${model.id}`);
|
|
242
|
+
console.error(`Query: ${args.query}`);
|
|
243
|
+
console.error("---");
|
|
244
|
+
// Start REPL
|
|
245
|
+
const repl = new PythonRepl();
|
|
246
|
+
const ac = new AbortController();
|
|
247
|
+
const abortAndExit = () => {
|
|
248
|
+
console.error("\nAborting...");
|
|
249
|
+
ac.abort();
|
|
250
|
+
};
|
|
251
|
+
process.on("SIGINT", abortAndExit);
|
|
252
|
+
process.on("SIGTERM", abortAndExit);
|
|
253
|
+
try {
|
|
254
|
+
await repl.start(ac.signal);
|
|
255
|
+
const startTime = Date.now();
|
|
256
|
+
const result = await runRlmLoop({
|
|
257
|
+
context,
|
|
258
|
+
query: args.query,
|
|
259
|
+
model,
|
|
260
|
+
repl,
|
|
261
|
+
signal: ac.signal,
|
|
262
|
+
onProgress: args.verbose
|
|
263
|
+
? (info) => {
|
|
264
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
265
|
+
console.error(`[${elapsed}s] Iteration ${info.iteration}/${info.maxIterations} | ` +
|
|
266
|
+
`Sub-queries: ${info.subQueries} | Phase: ${info.phase}`);
|
|
267
|
+
}
|
|
268
|
+
: undefined,
|
|
269
|
+
});
|
|
270
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
271
|
+
console.error("---");
|
|
272
|
+
console.error(`Completed in ${elapsed}s | ${result.iterations} iterations | ${result.totalSubQueries} sub-queries | ${result.completed ? "success" : "incomplete"}`);
|
|
273
|
+
console.error("---");
|
|
274
|
+
// Write the answer to stdout (not stderr) so it can be piped
|
|
275
|
+
console.log(result.answer);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
repl.shutdown();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
main().catch((err) => {
|
|
282
|
+
console.error("Fatal error:", err);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
});
|
|
285
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context compression — compresses thread output before returning to the orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Strategies:
|
|
5
|
+
* - structured (default): status + files + diff stats + key hunks + tail output
|
|
6
|
+
* - diff-only: just the git diff
|
|
7
|
+
* - truncate: raw truncation
|
|
8
|
+
* - llm-summary: use a cheap LLM to summarize thread work
|
|
9
|
+
*
|
|
10
|
+
* Episode quality: All strategies apply filterToSuccessfulOutput() first,
|
|
11
|
+
* stripping failed attempts, retries, and error noise so the orchestrator
|
|
12
|
+
* only sees what contributed to the final result (Slate-style episodes).
|
|
13
|
+
*/
|
|
14
|
+
import type { SwarmConfig } from "../core/types.js";
|
|
15
|
+
export interface CompressionInput {
|
|
16
|
+
agentOutput: string;
|
|
17
|
+
diff: string;
|
|
18
|
+
diffStats: string;
|
|
19
|
+
filesChanged: string[];
|
|
20
|
+
success: boolean;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Optional LLM summarizer for llm-summary strategy. */
|
|
25
|
+
export type LlmSummarizer = (text: string, instruction: string) => Promise<string>;
|
|
26
|
+
/** Register an LLM summarizer for the llm-summary compression strategy. */
|
|
27
|
+
export declare function setSummarizer(fn: LlmSummarizer): void;
|
|
28
|
+
export declare function compressResult(input: CompressionInput, strategy?: SwarmConfig["compression_strategy"], maxChars?: number): Promise<string>;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context compression — compresses thread output before returning to the orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Strategies:
|
|
5
|
+
* - structured (default): status + files + diff stats + key hunks + tail output
|
|
6
|
+
* - diff-only: just the git diff
|
|
7
|
+
* - truncate: raw truncation
|
|
8
|
+
* - llm-summary: use a cheap LLM to summarize thread work
|
|
9
|
+
*
|
|
10
|
+
* Episode quality: All strategies apply filterToSuccessfulOutput() first,
|
|
11
|
+
* stripping failed attempts, retries, and error noise so the orchestrator
|
|
12
|
+
* only sees what contributed to the final result (Slate-style episodes).
|
|
13
|
+
*/
|
|
14
|
+
// Module-level summarizer — set once by the swarm orchestrator
|
|
15
|
+
let _summarizer;
|
|
16
|
+
/** Register an LLM summarizer for the llm-summary compression strategy. */
|
|
17
|
+
export function setSummarizer(fn) {
|
|
18
|
+
_summarizer = fn;
|
|
19
|
+
}
|
|
20
|
+
// ── Episode quality filter ────────────────────────────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* Patterns that indicate failed attempts, retries, or noise in agent output.
|
|
23
|
+
* These lines are stripped so the orchestrator only sees successful conclusions.
|
|
24
|
+
*/
|
|
25
|
+
const FAILURE_NOISE_PATTERNS = [
|
|
26
|
+
// Error/retry indicators (anchored to avoid matching "Error handling added")
|
|
27
|
+
/^error: /i,
|
|
28
|
+
/^Error: /,
|
|
29
|
+
/^failed to /i,
|
|
30
|
+
/^retrying/i,
|
|
31
|
+
/^retry attempt/i,
|
|
32
|
+
/^warning: /i,
|
|
33
|
+
/^timed? ?out/i,
|
|
34
|
+
// Common agent noise: stack traces
|
|
35
|
+
/^\s+at\s+\S+\s+\(/,
|
|
36
|
+
/^Traceback \(most recent/,
|
|
37
|
+
/^\s+File ".*", line \d+/,
|
|
38
|
+
// Agent internal chatter
|
|
39
|
+
/^Thinking\.\.\./i,
|
|
40
|
+
/^Searching\.\.\./i,
|
|
41
|
+
/^Reading\.\.\./i,
|
|
42
|
+
/^Running command\.\.\./i,
|
|
43
|
+
// Reverted / undone actions (anchored to start of line)
|
|
44
|
+
/^reverted /i,
|
|
45
|
+
/^undoing /i,
|
|
46
|
+
/^rolling back/i,
|
|
47
|
+
];
|
|
48
|
+
/** Patterns that indicate successful, conclusive output worth keeping. */
|
|
49
|
+
const SUCCESS_SIGNAL_PATTERNS = [
|
|
50
|
+
/^Applied edit to/i,
|
|
51
|
+
/^Wrote /,
|
|
52
|
+
/^Created /i,
|
|
53
|
+
/^Updated /i,
|
|
54
|
+
/^Added /i,
|
|
55
|
+
/^Removed /i,
|
|
56
|
+
/^Fixed /i,
|
|
57
|
+
/^Committing /,
|
|
58
|
+
/tests? pass/i,
|
|
59
|
+
/✓|✔|PASS/,
|
|
60
|
+
/^DONE/i,
|
|
61
|
+
/^SUCCESS/i,
|
|
62
|
+
/^Result:/i,
|
|
63
|
+
/^Summary:/i,
|
|
64
|
+
/^Completed/i,
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Filter agent output to only include lines that contributed to the final result.
|
|
68
|
+
* Strips failed attempts, retries, stack traces, and noise.
|
|
69
|
+
* Keeps: success signals, file-change confirmations, final conclusions, and
|
|
70
|
+
* any line that doesn't match a known noise pattern.
|
|
71
|
+
*
|
|
72
|
+
* Strategy: remove known-bad lines rather than keep only known-good,
|
|
73
|
+
* so novel agent output is preserved by default.
|
|
74
|
+
*/
|
|
75
|
+
function filterToSuccessfulOutput(agentOutput) {
|
|
76
|
+
if (!agentOutput)
|
|
77
|
+
return agentOutput;
|
|
78
|
+
const lines = agentOutput.split("\n");
|
|
79
|
+
const filtered = [];
|
|
80
|
+
let inStackTrace = false;
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
const trimmed = line.trimStart();
|
|
83
|
+
// Detect start of stack trace blocks (test untrimmed `line` for JS stack traces with leading whitespace)
|
|
84
|
+
if (/^Traceback \(most recent/.test(trimmed) || /^\s+at\s+\S+\s+\(/.test(line)) {
|
|
85
|
+
inStackTrace = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// End stack trace on blank line or non-indented line
|
|
89
|
+
if (inStackTrace) {
|
|
90
|
+
if (trimmed === "" || (!trimmed.startsWith(" ") && !trimmed.startsWith("\t"))) {
|
|
91
|
+
inStackTrace = false;
|
|
92
|
+
// Still check if this line itself is noise
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
continue; // Skip stack trace continuation
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Always keep lines with success signals
|
|
99
|
+
if (SUCCESS_SIGNAL_PATTERNS.some((p) => p.test(trimmed))) {
|
|
100
|
+
filtered.push(line);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// Skip lines matching failure/noise patterns
|
|
104
|
+
if (FAILURE_NOISE_PATTERNS.some((p) => p.test(trimmed))) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Keep everything else (default: preserve novel output)
|
|
108
|
+
filtered.push(line);
|
|
109
|
+
}
|
|
110
|
+
// Collapse runs of blank lines to max 2
|
|
111
|
+
const collapsed = [];
|
|
112
|
+
let blankRun = 0;
|
|
113
|
+
for (const line of filtered) {
|
|
114
|
+
if (line.trim() === "") {
|
|
115
|
+
blankRun++;
|
|
116
|
+
if (blankRun <= 2)
|
|
117
|
+
collapsed.push(line);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
blankRun = 0;
|
|
121
|
+
collapsed.push(line);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return collapsed.join("\n").trim();
|
|
125
|
+
}
|
|
126
|
+
export async function compressResult(input, strategy = "structured", maxChars = 1000) {
|
|
127
|
+
// Episode quality: filter agent output to successful conclusions only
|
|
128
|
+
const filtered = {
|
|
129
|
+
...input,
|
|
130
|
+
agentOutput: filterToSuccessfulOutput(input.agentOutput),
|
|
131
|
+
};
|
|
132
|
+
switch (strategy) {
|
|
133
|
+
case "structured":
|
|
134
|
+
return compressStructured(filtered, maxChars);
|
|
135
|
+
case "diff-only":
|
|
136
|
+
return compressDiffOnly(filtered, maxChars);
|
|
137
|
+
case "truncate":
|
|
138
|
+
return compressTruncate(filtered, maxChars);
|
|
139
|
+
case "llm-summary":
|
|
140
|
+
return compressLlmSummary(filtered, maxChars);
|
|
141
|
+
default:
|
|
142
|
+
return compressStructured(filtered, maxChars);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function compressStructured(input, maxChars) {
|
|
146
|
+
const parts = [];
|
|
147
|
+
// Status line
|
|
148
|
+
parts.push(`Status: ${input.success ? "SUCCESS" : "FAILED"} (${(input.durationMs / 1000).toFixed(1)}s)`);
|
|
149
|
+
if (input.error) {
|
|
150
|
+
parts.push(`Error: ${input.error}`);
|
|
151
|
+
}
|
|
152
|
+
// Files changed
|
|
153
|
+
if (input.filesChanged.length > 0) {
|
|
154
|
+
parts.push(`Files changed (${input.filesChanged.length}):`);
|
|
155
|
+
for (const f of input.filesChanged.slice(0, 20)) {
|
|
156
|
+
parts.push(` - ${f}`);
|
|
157
|
+
}
|
|
158
|
+
if (input.filesChanged.length > 20) {
|
|
159
|
+
parts.push(` ... and ${input.filesChanged.length - 20} more`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Diff stats
|
|
163
|
+
if (input.diffStats && input.diffStats !== "(no changes)") {
|
|
164
|
+
parts.push(`\nDiff stats:\n${input.diffStats}`);
|
|
165
|
+
}
|
|
166
|
+
// Key diff hunks (first portion)
|
|
167
|
+
if (input.diff && input.diff !== "(no changes)") {
|
|
168
|
+
const diffBudget = Math.floor(maxChars * 0.4);
|
|
169
|
+
const truncatedDiff = input.diff.length > diffBudget ? `${input.diff.slice(0, diffBudget)}\n... [diff truncated]` : input.diff;
|
|
170
|
+
parts.push(`\nKey changes:\n${truncatedDiff}`);
|
|
171
|
+
}
|
|
172
|
+
// Agent output tail
|
|
173
|
+
if (input.agentOutput) {
|
|
174
|
+
const outputBudget = Math.floor(maxChars * 0.3);
|
|
175
|
+
const lines = input.agentOutput.split("\n");
|
|
176
|
+
const tail = lines.slice(-30).join("\n");
|
|
177
|
+
const truncatedOutput = tail.length > outputBudget ? tail.slice(-outputBudget) : tail;
|
|
178
|
+
parts.push(`\nAgent output (last 30 lines):\n${truncatedOutput}`);
|
|
179
|
+
}
|
|
180
|
+
const result = parts.join("\n");
|
|
181
|
+
// Final truncation safety (4x maxChars is the hard cap for combined sections)
|
|
182
|
+
if (result.length > maxChars * 4) {
|
|
183
|
+
return `${result.slice(0, maxChars * 4)}\n... [compressed output truncated]`;
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function compressDiffOnly(input, maxChars) {
|
|
188
|
+
const status = `Status: ${input.success ? "SUCCESS" : "FAILED"} (${(input.durationMs / 1000).toFixed(1)}s)`;
|
|
189
|
+
if (!input.diff || input.diff === "(no changes)") {
|
|
190
|
+
return `${status}\n(no changes)`;
|
|
191
|
+
}
|
|
192
|
+
if (input.diff.length > maxChars * 4) {
|
|
193
|
+
return `${status}\n${input.diff.slice(0, maxChars * 4)}\n... [diff truncated]`;
|
|
194
|
+
}
|
|
195
|
+
return `${status}\n${input.diff}`;
|
|
196
|
+
}
|
|
197
|
+
function compressTruncate(input, maxChars) {
|
|
198
|
+
const raw = [`Status: ${input.success ? "SUCCESS" : "FAILED"}`, input.agentOutput, input.diff]
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.join("\n\n");
|
|
201
|
+
if (raw.length > maxChars * 4) {
|
|
202
|
+
// Preserve status line at the head, truncate from the middle
|
|
203
|
+
const statusEnd = raw.indexOf("\n");
|
|
204
|
+
const statusLine = statusEnd !== -1 ? raw.slice(0, statusEnd) : "";
|
|
205
|
+
const remaining = maxChars * 4 - statusLine.length - 20;
|
|
206
|
+
return `${statusLine}\n... [truncated]\n${raw.slice(-remaining)}`;
|
|
207
|
+
}
|
|
208
|
+
return raw;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* LLM-based compression — uses a cheap model to summarize the thread's work.
|
|
212
|
+
* Falls back to structured compression if no summarizer is registered.
|
|
213
|
+
*/
|
|
214
|
+
async function compressLlmSummary(input, maxChars) {
|
|
215
|
+
if (!_summarizer) {
|
|
216
|
+
// Fall back to structured if no summarizer available
|
|
217
|
+
return compressStructured(input, maxChars);
|
|
218
|
+
}
|
|
219
|
+
// Build the text to summarize
|
|
220
|
+
const parts = [];
|
|
221
|
+
parts.push(`Task outcome: ${input.success ? "SUCCESS" : "FAILED"} (${(input.durationMs / 1000).toFixed(1)}s)`);
|
|
222
|
+
if (input.error) {
|
|
223
|
+
parts.push(`Error: ${input.error}`);
|
|
224
|
+
}
|
|
225
|
+
if (input.filesChanged.length > 0) {
|
|
226
|
+
parts.push(`Files changed: ${input.filesChanged.join(", ")}`);
|
|
227
|
+
}
|
|
228
|
+
if (input.diffStats && input.diffStats !== "(no changes)") {
|
|
229
|
+
parts.push(`Diff stats:\n${input.diffStats}`);
|
|
230
|
+
}
|
|
231
|
+
// Include truncated diff for context
|
|
232
|
+
if (input.diff && input.diff !== "(no changes)") {
|
|
233
|
+
const diffSlice = input.diff.slice(0, maxChars * 2);
|
|
234
|
+
parts.push(`Diff:\n${diffSlice}`);
|
|
235
|
+
}
|
|
236
|
+
// Include truncated agent output
|
|
237
|
+
if (input.agentOutput) {
|
|
238
|
+
const outputSlice = input.agentOutput.slice(-maxChars);
|
|
239
|
+
parts.push(`Agent output (tail):\n${outputSlice}`);
|
|
240
|
+
}
|
|
241
|
+
const textToSummarize = parts.join("\n\n");
|
|
242
|
+
const instruction = [
|
|
243
|
+
"Summarize this coding agent thread result concisely.",
|
|
244
|
+
"Include: what was done, which files were changed, whether it succeeded, and any key details.",
|
|
245
|
+
`Keep the summary under ${maxChars} characters.`,
|
|
246
|
+
"Be specific about code changes — mention function names, patterns, and key decisions.",
|
|
247
|
+
].join(" ");
|
|
248
|
+
try {
|
|
249
|
+
const summary = await _summarizer(textToSummarize, instruction);
|
|
250
|
+
// Prepend status line
|
|
251
|
+
const status = `Status: ${input.success ? "SUCCESS" : "FAILED"} (${(input.durationMs / 1000).toFixed(1)}s)`;
|
|
252
|
+
const filesLine = input.filesChanged.length > 0 ? `\nFiles: ${input.filesChanged.join(", ")}` : "";
|
|
253
|
+
const result = `${status}${filesLine}\n\n${summary}`;
|
|
254
|
+
// Safety cap
|
|
255
|
+
if (result.length > maxChars * 2) {
|
|
256
|
+
return `${result.slice(0, maxChars * 2)}\n... [summary truncated]`;
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Fall back to structured on LLM failure
|
|
262
|
+
return compressStructured(input, maxChars);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=compressor.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader for swarm-code.
|
|
3
|
+
*
|
|
4
|
+
* Reads swarm_config.yaml (or rlm_config.yaml fallback) from project root or cwd.
|
|
5
|
+
* Extends RLM config with swarm-specific fields.
|
|
6
|
+
*/
|
|
7
|
+
/** Named model slot overrides — lets users assign specific models per task type. */
|
|
8
|
+
export interface ModelSlots {
|
|
9
|
+
execution: string;
|
|
10
|
+
search: string;
|
|
11
|
+
reasoning: string;
|
|
12
|
+
planning: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SwarmConfig {
|
|
15
|
+
max_iterations: number;
|
|
16
|
+
max_depth: number;
|
|
17
|
+
max_sub_queries: number;
|
|
18
|
+
truncate_len: number;
|
|
19
|
+
metadata_preview_lines: number;
|
|
20
|
+
max_threads: number;
|
|
21
|
+
max_total_threads: number;
|
|
22
|
+
thread_timeout_ms: number;
|
|
23
|
+
max_thread_budget_usd: number;
|
|
24
|
+
max_session_budget_usd: number;
|
|
25
|
+
default_agent: string;
|
|
26
|
+
default_model: string;
|
|
27
|
+
auto_model_selection: boolean;
|
|
28
|
+
compression_strategy: "structured" | "llm-summary" | "diff-only" | "truncate";
|
|
29
|
+
compression_max_tokens: number;
|
|
30
|
+
worktree_base_dir: string;
|
|
31
|
+
auto_cleanup_worktrees: boolean;
|
|
32
|
+
episodic_memory_enabled: boolean;
|
|
33
|
+
memory_dir: string;
|
|
34
|
+
thread_retries: number;
|
|
35
|
+
model_slots: ModelSlots;
|
|
36
|
+
thread_cache_persist: boolean;
|
|
37
|
+
thread_cache_dir: string;
|
|
38
|
+
thread_cache_ttl_hours: number;
|
|
39
|
+
opencode_server_mode: boolean;
|
|
40
|
+
}
|
|
41
|
+
export type RlmConfig = SwarmConfig;
|
|
42
|
+
export declare function loadConfig(cwd?: string): SwarmConfig;
|