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
@@ -0,0 +1,409 @@
1
+ /**
2
+ * RLM Loop — implements Algorithm 1 from "Recursive Language Models" (arXiv:2512.24601).
3
+ * Extended for swarm-code with thread handler wiring.
4
+ *
5
+ * The loop works as follows:
6
+ * 1. Inject the full context into a persistent Python REPL as a variable.
7
+ * 2. Send the LLM metadata about the context plus the user's query.
8
+ * The LLM writes Python code that can inspect/slice/query `context`,
9
+ * call `llm_query()` recursively, call `thread()` to spawn agents,
10
+ * and call FINAL() when done.
11
+ * 3. Execute the code, capture stdout.
12
+ * 4. If FINAL is set, return it. Otherwise loop.
13
+ */
14
+ import { completeSimple, } from "@mariozechner/pi-ai";
15
+ import { loadConfig } from "../config.js";
16
+ // ── Load config ─────────────────────────────────────────────────────────────
17
+ const config = loadConfig();
18
+ // ── Default system prompt (RLM text-processing mode) ────────────────────────
19
+ function buildDefaultSystemPrompt() {
20
+ return `You are a Recursive Language Model (RLM) agent. You process large contexts by writing Python code that runs in a persistent REPL.
21
+
22
+ ## Available in the REPL
23
+
24
+ 1. A \`context\` variable containing the full input text (may be very large). You should check the content of the \`context\` variable to understand what you are working with.
25
+
26
+ 2. A \`llm_query(sub_context, instruction)\` function that sends a sub-piece of the context to an LLM with an instruction and returns the response. Use this for summarization, extraction, classification, etc. on chunks. For parallel queries, use \`async_llm_query()\` with \`asyncio.gather()\`.
27
+
28
+ 3. Two functions to return your answer:
29
+ - \`FINAL("your answer")\` — provide the answer as a string
30
+ - \`FINAL_VAR(variable)\` — return a variable you built up in the REPL
31
+
32
+ ## Rules
33
+
34
+ 1. Write valid Python 3 code. You have access to the standard library.
35
+ 2. Use \`print()\` to output metadata/intermediate results visible in the next iteration.
36
+ 3. Use \`len(context)\` and slicing to understand the context size before processing.
37
+ 4. For large contexts, split into chunks and use \`llm_query()\` on each chunk, then aggregate.
38
+ 5. Call \`FINAL("answer")\` or \`FINAL_VAR(var)\` only when you have a complete answer.
39
+ 6. Do NOT call FINAL prematurely — if you need more iterations, just print your intermediate state.
40
+ 7. Be efficient: minimize the number of \`llm_query()\` calls by using smart chunking.
41
+ 8. Print output will be truncated to last ${config.truncate_len} characters. Keep printed output concise.
42
+
43
+ ## How to control sub-agent behavior
44
+
45
+ - When calling \`llm_query()\`, give clear instructions at the beginning of the context. If you only pass context without instructions, the sub-agent cannot do its task.
46
+ - To extract data verbatim: instruct the sub-agent to use \`FINAL_VAR\` and slice important sections.
47
+ - To summarize or analyze: instruct the sub-agent to explore and generate the answer.
48
+ - Help sub-agents by describing the data format (dict, list, etc.) — clarity is important!
49
+
50
+ ## Important notes
51
+
52
+ - This is a multi-turn environment. You do NOT need to answer in one shot.
53
+ - Before returning via FINAL, it is advisable to print the answer first to inspect formatting.
54
+ - The REPL persists state like a Jupyter notebook — past variables and code are maintained. Do NOT rewrite old code or accidentally delete the \`context\` variable.
55
+ - You will only see truncated outputs, so use \`llm_query()\` for semantic analysis of large text.
56
+ - You can use variables as buffers to build up your final answer across iterations.
57
+
58
+ ## Output format
59
+
60
+ Respond with ONLY a Python code block. No explanation before or after.
61
+
62
+ \`\`\`python
63
+ # Your working python code
64
+ print(f"Context length: {len(context)} chars")
65
+ \`\`\`
66
+
67
+ ## Example strategies
68
+
69
+ **Chunking for large contexts:**
70
+ \`\`\`python
71
+ chunk_size = len(context) // 5
72
+ buffers = []
73
+ for i in range(5):
74
+ start = i * chunk_size
75
+ end = (i + 1) * chunk_size if i < 4 else len(context)
76
+ chunk = context[start:end]
77
+ result = llm_query(chunk, f"Extract key information relevant to: {query}")
78
+ buffers.append(result)
79
+ print(f"Chunk {i+1}/5 done: {len(result)} chars")
80
+ \`\`\`
81
+
82
+ **Parallel queries with asyncio:**
83
+ \`\`\`python
84
+ import asyncio
85
+ tasks = []
86
+ for i, chunk in enumerate(chunks):
87
+ tasks.append(async_llm_query(chunk, f"Summarize chunk {i}"))
88
+ results = await asyncio.gather(*tasks)
89
+ \`\`\`
90
+
91
+ **Building up a final answer:**
92
+ \`\`\`python
93
+ # After collecting all results in a buffer
94
+ final_answer = llm_query("\\n".join(buffers), f"Synthesize these summaries to answer: {query}")
95
+ FINAL(final_answer)
96
+ \`\`\``;
97
+ }
98
+ // ── Abort helper ────────────────────────────────────────────────────────
99
+ /** Race a promise against an AbortSignal so Ctrl+C cancels long API calls. */
100
+ function raceAbort(promise, signal) {
101
+ if (!signal)
102
+ return promise;
103
+ if (signal.aborted)
104
+ return Promise.reject(new Error("Aborted"));
105
+ let onAbort;
106
+ const abortPromise = new Promise((_, reject) => {
107
+ onAbort = () => reject(new Error("Aborted"));
108
+ signal.addEventListener("abort", onAbort, { once: true });
109
+ });
110
+ return Promise.race([promise, abortPromise]).finally(() => {
111
+ if (onAbort)
112
+ signal.removeEventListener("abort", onAbort);
113
+ });
114
+ }
115
+ // ── Helpers ─────────────────────────────────────────────────────────────────
116
+ function buildContextMetadata(context) {
117
+ const lines = context.split("\n");
118
+ const charCount = context.length;
119
+ const lineCount = lines.length;
120
+ const previewStart = lines.slice(0, config.metadata_preview_lines).join("\n");
121
+ const previewEnd = lines.slice(-config.metadata_preview_lines).join("\n");
122
+ return [
123
+ `Context statistics:`,
124
+ ` - ${charCount.toLocaleString()} characters`,
125
+ ` - ${lineCount.toLocaleString()} lines`,
126
+ ``,
127
+ `First ${config.metadata_preview_lines} lines:`,
128
+ previewStart,
129
+ ``,
130
+ `Last ${config.metadata_preview_lines} lines:`,
131
+ previewEnd,
132
+ ].join("\n");
133
+ }
134
+ function extractCodeFromResponse(response) {
135
+ for (const block of response.content) {
136
+ if (block.type !== "text")
137
+ continue;
138
+ const text = block.text;
139
+ // Try ```python or ```repl blocks
140
+ const fenceMatch = text.match(/```(?:python|repl)?\s*\n([\s\S]*?)```/);
141
+ if (fenceMatch)
142
+ return fenceMatch[1].trim();
143
+ // Fallback: if the response looks like raw Python code (require Python-specific patterns)
144
+ const trimmed = text.trim();
145
+ if (trimmed &&
146
+ !trimmed.startsWith("#") &&
147
+ (trimmed.includes("print(") ||
148
+ trimmed.includes("import ") ||
149
+ (trimmed.includes("for ") && trimmed.includes(":")) ||
150
+ (trimmed.includes("def ") && trimmed.includes(":")) ||
151
+ trimmed.includes("FINAL(") ||
152
+ trimmed.includes("llm_query(") ||
153
+ trimmed.includes("thread(") ||
154
+ trimmed.includes("async_thread(") ||
155
+ trimmed.includes("merge_threads("))) {
156
+ return trimmed;
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+ function truncateOutput(text) {
162
+ if (text.length <= config.truncate_len) {
163
+ if (text.length === 0)
164
+ return "[EMPTY OUTPUT]";
165
+ return text;
166
+ }
167
+ return `[TRUNCATED: Last ${config.truncate_len} chars shown].. ${text.slice(-config.truncate_len)}`;
168
+ }
169
+ // ── Main loop ───────────────────────────────────────────────────────────────
170
+ export async function runRlmLoop(options) {
171
+ const { context, query, model, repl, signal, onProgress, onSubQueryStart, onSubQuery, systemPrompt, threadHandler, mergeHandler, } = options;
172
+ let totalSubQueries = 0;
173
+ let iterationSubQueries = 0;
174
+ const llmQueryHandler = async (subContext, instruction) => {
175
+ if (signal?.aborted)
176
+ throw new Error("Aborted");
177
+ if (totalSubQueries >= config.max_sub_queries) {
178
+ return `[ERROR] Maximum sub-query limit (${config.max_sub_queries}) reached. Call FINAL() with your best answer.`;
179
+ }
180
+ ++totalSubQueries;
181
+ const queryIndex = ++iterationSubQueries;
182
+ const sqStart = Date.now();
183
+ onSubQueryStart?.({
184
+ index: queryIndex,
185
+ contextLength: subContext.length,
186
+ instruction,
187
+ });
188
+ const response = await raceAbort(completeSimple(model, {
189
+ systemPrompt: `You are a helpful assistant. Answer the user's question based on the provided context. Respond in natural language (not code). Be concise but thorough.`,
190
+ messages: [
191
+ {
192
+ role: "user",
193
+ content: `Context:\n${subContext}\n\nInstruction: ${instruction}`,
194
+ timestamp: Date.now(),
195
+ },
196
+ ],
197
+ }), signal);
198
+ const textParts = response.content.filter((b) => b.type === "text").map((b) => b.text);
199
+ const result = textParts.join("\n");
200
+ onSubQuery?.({
201
+ index: queryIndex,
202
+ contextLength: subContext.length,
203
+ instruction,
204
+ resultLength: result.length,
205
+ resultPreview: result,
206
+ elapsedMs: Date.now() - sqStart,
207
+ });
208
+ return result;
209
+ };
210
+ /** Set up (or re-set up) the REPL with context and handler. */
211
+ async function initRepl() {
212
+ await repl.setContext(context);
213
+ await repl.resetFinal();
214
+ repl.setLlmQueryHandler(llmQueryHandler);
215
+ // Wire thread and merge handlers if provided (swarm mode)
216
+ if (threadHandler) {
217
+ repl.setThreadHandler(threadHandler);
218
+ }
219
+ if (mergeHandler) {
220
+ repl.setMergeHandler(mergeHandler);
221
+ }
222
+ }
223
+ await initRepl();
224
+ const activeSystemPrompt = systemPrompt || buildDefaultSystemPrompt();
225
+ const metadata = buildContextMetadata(context);
226
+ const conversationHistory = [
227
+ {
228
+ role: "user",
229
+ content: `${metadata}\n\nQuery: ${query}`,
230
+ timestamp: Date.now(),
231
+ },
232
+ ];
233
+ for (let iteration = 1; iteration <= config.max_iterations; iteration++) {
234
+ iterationSubQueries = 0;
235
+ if (signal?.aborted) {
236
+ return { answer: "[Aborted]", iterations: iteration, totalSubQueries, completed: false };
237
+ }
238
+ const lastUserMsg = conversationHistory.filter((m) => m.role === "user").at(-1);
239
+ const userMsgText = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
240
+ onProgress?.({
241
+ iteration,
242
+ maxIterations: config.max_iterations,
243
+ subQueries: totalSubQueries,
244
+ phase: "generating_code",
245
+ userMessage: userMsgText,
246
+ systemPrompt: iteration === 1 ? activeSystemPrompt : undefined,
247
+ });
248
+ let response;
249
+ try {
250
+ response = await raceAbort(completeSimple(model, {
251
+ systemPrompt: activeSystemPrompt,
252
+ messages: conversationHistory,
253
+ }), signal);
254
+ }
255
+ catch (apiErr) {
256
+ if (signal?.aborted) {
257
+ return { answer: "[Aborted]", iterations: iteration, totalSubQueries, completed: false };
258
+ }
259
+ const errMsg = apiErr instanceof Error ? apiErr.message : String(apiErr);
260
+ return {
261
+ answer: `[API Error] ${errMsg}`,
262
+ iterations: iteration,
263
+ totalSubQueries,
264
+ completed: false,
265
+ };
266
+ }
267
+ if (signal?.aborted) {
268
+ return { answer: "[Aborted]", iterations: iteration, totalSubQueries, completed: false };
269
+ }
270
+ // Surface API errors — bail immediately on unrecoverable errors
271
+ if ("errorMessage" in response && response.errorMessage) {
272
+ const errMsg = response.errorMessage;
273
+ const isAuth = errMsg.includes("authentication") || errMsg.includes("401");
274
+ const isQuota = errMsg.includes("quota") || errMsg.includes("billing") || errMsg.includes("429") || errMsg.includes("rate");
275
+ const isServer = errMsg.includes("500") || errMsg.includes("502") || errMsg.includes("503") || errMsg.includes("overloaded");
276
+ if (isAuth) {
277
+ return {
278
+ answer: `[API Error] ${errMsg}\n\nCheck your API key in .env or run /provider to reconfigure.`,
279
+ iterations: iteration,
280
+ totalSubQueries,
281
+ completed: false,
282
+ };
283
+ }
284
+ if (isQuota) {
285
+ return {
286
+ answer: `[API Error] ${errMsg}\n\nCheck your plan and billing at your provider's dashboard.`,
287
+ iterations: iteration,
288
+ totalSubQueries,
289
+ completed: false,
290
+ };
291
+ }
292
+ if (isServer) {
293
+ return {
294
+ answer: `[API Error] ${errMsg}\n\nThe provider's API is currently unavailable. Try again later.`,
295
+ iterations: iteration,
296
+ totalSubQueries,
297
+ completed: false,
298
+ };
299
+ }
300
+ // Unknown API error — still bail, don't waste iterations
301
+ return {
302
+ answer: `[API Error] ${errMsg}`,
303
+ iterations: iteration,
304
+ totalSubQueries,
305
+ completed: false,
306
+ };
307
+ }
308
+ const rawResponseText = response.content
309
+ .filter((b) => b.type === "text")
310
+ .map((b) => b.text)
311
+ .join("\n");
312
+ const code = extractCodeFromResponse(response);
313
+ if (!code) {
314
+ // No code block found — might be a direct answer or extraction failure
315
+ conversationHistory.push(response);
316
+ conversationHistory.push({
317
+ role: "user",
318
+ content: "Error: Could not extract code. Make sure to wrap your code in ```python ... ``` blocks.",
319
+ timestamp: Date.now(),
320
+ });
321
+ continue;
322
+ }
323
+ conversationHistory.push(response);
324
+ onProgress?.({
325
+ iteration,
326
+ maxIterations: config.max_iterations,
327
+ subQueries: totalSubQueries,
328
+ phase: "executing",
329
+ code,
330
+ rawResponse: rawResponseText,
331
+ });
332
+ let execResult;
333
+ try {
334
+ execResult = await repl.execute(code);
335
+ }
336
+ catch (err) {
337
+ if (signal?.aborted) {
338
+ return { answer: "[Aborted]", iterations: iteration, totalSubQueries, completed: false };
339
+ }
340
+ const errorMsg = err instanceof Error ? err.message : String(err);
341
+ // If the REPL timed out or crashed, restart it so next iteration works
342
+ if (errorMsg.includes("Timeout") || errorMsg.includes("not running") || errorMsg.includes("shut down")) {
343
+ try {
344
+ repl.shutdown();
345
+ await repl.start(signal);
346
+ await initRepl();
347
+ }
348
+ catch {
349
+ return {
350
+ answer: "[REPL crashed and could not restart]",
351
+ iterations: iteration,
352
+ totalSubQueries,
353
+ completed: false,
354
+ };
355
+ }
356
+ }
357
+ conversationHistory.push({
358
+ role: "user",
359
+ content: `Execution error: ${errorMsg}\n\nPlease fix the code and try again.`,
360
+ timestamp: Date.now(),
361
+ });
362
+ continue;
363
+ }
364
+ if (signal?.aborted) {
365
+ return { answer: "[Aborted]", iterations: iteration, totalSubQueries, completed: false };
366
+ }
367
+ onProgress?.({
368
+ iteration,
369
+ maxIterations: config.max_iterations,
370
+ subQueries: totalSubQueries,
371
+ phase: "checking_final",
372
+ stdout: execResult.stdout,
373
+ stderr: execResult.stderr,
374
+ });
375
+ if (execResult.hasFinal && execResult.finalValue !== null) {
376
+ return {
377
+ answer: execResult.finalValue,
378
+ iterations: iteration,
379
+ totalSubQueries,
380
+ completed: true,
381
+ };
382
+ }
383
+ // Build next user message with truncated output
384
+ const parts = [];
385
+ if (execResult.stdout) {
386
+ parts.push(`Output:\n${truncateOutput(execResult.stdout)}`);
387
+ }
388
+ if (execResult.stderr) {
389
+ parts.push(`Stderr:\n${execResult.stderr.slice(0, 5000)}`);
390
+ }
391
+ if (parts.length === 0) {
392
+ parts.push("(No output produced. The code ran without printing anything.)");
393
+ }
394
+ parts.push(`\nIteration ${iteration}/${config.max_iterations}. Sub-queries used: ${totalSubQueries}/${config.max_sub_queries}.`);
395
+ parts.push("Continue processing or call FINAL() when you have the answer.");
396
+ conversationHistory.push({
397
+ role: "user",
398
+ content: parts.join("\n\n"),
399
+ timestamp: Date.now(),
400
+ });
401
+ }
402
+ return {
403
+ answer: "[Maximum iterations reached without calling FINAL]",
404
+ iterations: config.max_iterations,
405
+ totalSubQueries,
406
+ completed: false,
407
+ };
408
+ }
409
+ //# sourceMappingURL=rlm.js.map