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/config.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
// RLM defaults
|
|
13
|
+
max_iterations: 20,
|
|
14
|
+
max_depth: 3,
|
|
15
|
+
max_sub_queries: 50,
|
|
16
|
+
truncate_len: 5000,
|
|
17
|
+
metadata_preview_lines: 20,
|
|
18
|
+
// Swarm defaults
|
|
19
|
+
max_threads: 5,
|
|
20
|
+
max_total_threads: 20,
|
|
21
|
+
thread_timeout_ms: 300000,
|
|
22
|
+
max_thread_budget_usd: 1.0,
|
|
23
|
+
max_session_budget_usd: 10.0,
|
|
24
|
+
default_agent: "opencode",
|
|
25
|
+
default_model: "anthropic/claude-sonnet-4-6",
|
|
26
|
+
auto_model_selection: false,
|
|
27
|
+
compression_strategy: "structured",
|
|
28
|
+
compression_max_tokens: 1000,
|
|
29
|
+
worktree_base_dir: ".swarm-worktrees",
|
|
30
|
+
auto_cleanup_worktrees: true,
|
|
31
|
+
episodic_memory_enabled: false,
|
|
32
|
+
memory_dir: path.join(os.homedir(), ".swarm", "memory"),
|
|
33
|
+
thread_retries: 1,
|
|
34
|
+
model_slots: {
|
|
35
|
+
execution: "", // empty = use agent's default based on complexity
|
|
36
|
+
search: "",
|
|
37
|
+
reasoning: "",
|
|
38
|
+
planning: "",
|
|
39
|
+
},
|
|
40
|
+
thread_cache_persist: false,
|
|
41
|
+
thread_cache_dir: path.join(os.homedir(), ".swarm", "cache"),
|
|
42
|
+
thread_cache_ttl_hours: 24,
|
|
43
|
+
opencode_server_mode: true,
|
|
44
|
+
};
|
|
45
|
+
function parseYaml(text) {
|
|
46
|
+
// Minimal YAML parser for flat key:value files (no nested objects, no arrays)
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const line of text.split("\n")) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
51
|
+
continue;
|
|
52
|
+
const colonIdx = trimmed.indexOf(":");
|
|
53
|
+
if (colonIdx === -1)
|
|
54
|
+
continue;
|
|
55
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
56
|
+
const rawVal = trimmed.slice(colonIdx + 1).trim();
|
|
57
|
+
// Strip inline comments (but not inside quoted strings)
|
|
58
|
+
let val;
|
|
59
|
+
if ((rawVal.startsWith('"') && rawVal.includes('"', 1)) || (rawVal.startsWith("'") && rawVal.includes("'", 1))) {
|
|
60
|
+
// Quoted value — don't strip inline comments
|
|
61
|
+
val = rawVal;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
val = rawVal.replace(/\s+#.*$/, "");
|
|
65
|
+
}
|
|
66
|
+
// Parse number
|
|
67
|
+
const num = Number(val);
|
|
68
|
+
if (!Number.isNaN(num) && val !== "") {
|
|
69
|
+
result[key] = num;
|
|
70
|
+
}
|
|
71
|
+
else if (val === "true") {
|
|
72
|
+
result[key] = true;
|
|
73
|
+
}
|
|
74
|
+
else if (val === "false") {
|
|
75
|
+
result[key] = false;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Strip quotes
|
|
79
|
+
result[key] = val.replace(/^["']|["']$/g, "");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
export function loadConfig(cwd) {
|
|
85
|
+
// Search order: cwd swarm_config, cwd rlm_config, package root swarm_config, package root rlm_config
|
|
86
|
+
// User prefs (~/.swarm/config.yaml) are overlaid on top of the first found project config
|
|
87
|
+
const projectDir = cwd || process.cwd();
|
|
88
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
89
|
+
const userConfigPath = path.join(os.homedir(), ".swarm", "config.yaml");
|
|
90
|
+
const candidates = [
|
|
91
|
+
path.resolve(projectDir, "swarm_config.yaml"),
|
|
92
|
+
path.resolve(projectDir, "rlm_config.yaml"),
|
|
93
|
+
path.resolve(pkgRoot, "swarm_config.yaml"),
|
|
94
|
+
path.resolve(pkgRoot, "rlm_config.yaml"),
|
|
95
|
+
];
|
|
96
|
+
for (const configPath of candidates) {
|
|
97
|
+
if (fs.existsSync(configPath)) {
|
|
98
|
+
try {
|
|
99
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
100
|
+
let parsed = parseYaml(raw);
|
|
101
|
+
// Overlay user preferences from ~/.swarm/config.yaml
|
|
102
|
+
if (fs.existsSync(userConfigPath)) {
|
|
103
|
+
try {
|
|
104
|
+
const userRaw = fs.readFileSync(userConfigPath, "utf-8");
|
|
105
|
+
const userParsed = parseYaml(userRaw);
|
|
106
|
+
parsed = { ...parsed, ...userParsed };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* ignore malformed user config */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const clamp = (v, min, max, def) => typeof v === "number" && Number.isFinite(v) ? Math.max(min, Math.min(max, Math.round(v))) : def;
|
|
113
|
+
const str = (v, def) => (typeof v === "string" && v.length > 0 ? v : def);
|
|
114
|
+
const bool = (v, def) => (typeof v === "boolean" ? v : def);
|
|
115
|
+
const validStrategies = ["structured", "llm-summary", "diff-only", "truncate"];
|
|
116
|
+
const strategyVal = str(parsed.compression_strategy, DEFAULTS.compression_strategy);
|
|
117
|
+
const strategy = validStrategies.includes(strategyVal)
|
|
118
|
+
? strategyVal
|
|
119
|
+
: DEFAULTS.compression_strategy;
|
|
120
|
+
return {
|
|
121
|
+
// RLM fields
|
|
122
|
+
max_iterations: clamp(parsed.max_iterations, 1, 100, DEFAULTS.max_iterations),
|
|
123
|
+
max_depth: clamp(parsed.max_depth, 1, 10, DEFAULTS.max_depth),
|
|
124
|
+
max_sub_queries: clamp(parsed.max_sub_queries, 1, 500, DEFAULTS.max_sub_queries),
|
|
125
|
+
truncate_len: clamp(parsed.truncate_len, 500, 50000, DEFAULTS.truncate_len),
|
|
126
|
+
metadata_preview_lines: clamp(parsed.metadata_preview_lines, 5, 100, DEFAULTS.metadata_preview_lines),
|
|
127
|
+
// Swarm fields
|
|
128
|
+
max_threads: clamp(parsed.max_threads, 1, 20, DEFAULTS.max_threads),
|
|
129
|
+
max_total_threads: clamp(parsed.max_total_threads, 1, 100, DEFAULTS.max_total_threads),
|
|
130
|
+
thread_timeout_ms: clamp(parsed.thread_timeout_ms, 10000, 3600000, DEFAULTS.thread_timeout_ms),
|
|
131
|
+
max_thread_budget_usd: typeof parsed.max_thread_budget_usd === "number" &&
|
|
132
|
+
Number.isFinite(parsed.max_thread_budget_usd) &&
|
|
133
|
+
parsed.max_thread_budget_usd > 0
|
|
134
|
+
? parsed.max_thread_budget_usd
|
|
135
|
+
: DEFAULTS.max_thread_budget_usd,
|
|
136
|
+
max_session_budget_usd: typeof parsed.max_session_budget_usd === "number" &&
|
|
137
|
+
Number.isFinite(parsed.max_session_budget_usd) &&
|
|
138
|
+
parsed.max_session_budget_usd > 0
|
|
139
|
+
? parsed.max_session_budget_usd
|
|
140
|
+
: DEFAULTS.max_session_budget_usd,
|
|
141
|
+
default_agent: str(parsed.default_agent, DEFAULTS.default_agent),
|
|
142
|
+
default_model: str(parsed.default_model, DEFAULTS.default_model),
|
|
143
|
+
auto_model_selection: bool(parsed.auto_model_selection, DEFAULTS.auto_model_selection),
|
|
144
|
+
compression_strategy: strategy,
|
|
145
|
+
compression_max_tokens: clamp(parsed.compression_max_tokens, 100, 10000, DEFAULTS.compression_max_tokens),
|
|
146
|
+
worktree_base_dir: str(parsed.worktree_base_dir, DEFAULTS.worktree_base_dir),
|
|
147
|
+
auto_cleanup_worktrees: bool(parsed.auto_cleanup_worktrees, DEFAULTS.auto_cleanup_worktrees),
|
|
148
|
+
episodic_memory_enabled: bool(parsed.episodic_memory_enabled, DEFAULTS.episodic_memory_enabled),
|
|
149
|
+
memory_dir: str(parsed.memory_dir, DEFAULTS.memory_dir),
|
|
150
|
+
thread_retries: clamp(parsed.thread_retries, 0, 3, DEFAULTS.thread_retries),
|
|
151
|
+
model_slots: {
|
|
152
|
+
execution: str(parsed.model_slot_execution, DEFAULTS.model_slots.execution),
|
|
153
|
+
search: str(parsed.model_slot_search, DEFAULTS.model_slots.search),
|
|
154
|
+
reasoning: str(parsed.model_slot_reasoning, DEFAULTS.model_slots.reasoning),
|
|
155
|
+
planning: str(parsed.model_slot_planning, DEFAULTS.model_slots.planning),
|
|
156
|
+
},
|
|
157
|
+
thread_cache_persist: bool(parsed.thread_cache_persist, DEFAULTS.thread_cache_persist),
|
|
158
|
+
thread_cache_dir: str(parsed.thread_cache_dir, DEFAULTS.thread_cache_dir),
|
|
159
|
+
thread_cache_ttl_hours: clamp(parsed.thread_cache_ttl_hours, 1, 720, DEFAULTS.thread_cache_ttl_hours),
|
|
160
|
+
opencode_server_mode: bool(parsed.opencode_server_mode, DEFAULTS.opencode_server_mode),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Fall through to defaults
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { ...DEFAULTS };
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Python REPL manager for swarm-code.
|
|
3
|
+
*
|
|
4
|
+
* Spawns a single Python subprocess running `runtime.py` and keeps it alive
|
|
5
|
+
* across multiple RLM iterations. Communication uses line-delimited JSON
|
|
6
|
+
* over stdin/stdout.
|
|
7
|
+
*
|
|
8
|
+
* Extended from rlm-cli with thread_request and merge_request handlers.
|
|
9
|
+
*/
|
|
10
|
+
/** Result of executing a code snippet in the REPL. */
|
|
11
|
+
export interface ExecResult {
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
hasFinal: boolean;
|
|
15
|
+
finalValue: string | null;
|
|
16
|
+
}
|
|
17
|
+
/** Callback the host provides to handle llm_query() calls from Python. */
|
|
18
|
+
export type LlmQueryHandler = (subContext: string, instruction: string) => Promise<string>;
|
|
19
|
+
/** Callback the host provides to handle thread() calls from Python. */
|
|
20
|
+
export type ThreadHandler = (task: string, context: string, agentBackend: string, model: string, files: string[]) => Promise<{
|
|
21
|
+
result: string;
|
|
22
|
+
success: boolean;
|
|
23
|
+
filesChanged: string[];
|
|
24
|
+
durationMs: number;
|
|
25
|
+
}>;
|
|
26
|
+
/** Callback the host provides to handle merge_threads() calls from Python. */
|
|
27
|
+
export type MergeHandler = () => Promise<{
|
|
28
|
+
result: string;
|
|
29
|
+
success: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
export declare class PythonRepl {
|
|
32
|
+
private proc;
|
|
33
|
+
private rl;
|
|
34
|
+
private llmQueryHandler;
|
|
35
|
+
private threadHandler;
|
|
36
|
+
private mergeHandler;
|
|
37
|
+
/**
|
|
38
|
+
* Pending resolvers for messages we're waiting on from Python.
|
|
39
|
+
* Each entry maps a message type to a one-shot resolve/reject pair.
|
|
40
|
+
*/
|
|
41
|
+
private pending;
|
|
42
|
+
/** Whether the REPL subprocess is alive. */
|
|
43
|
+
get isAlive(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Start the Python subprocess and wait for it to signal readiness.
|
|
46
|
+
*/
|
|
47
|
+
start(signal?: AbortSignal): Promise<void>;
|
|
48
|
+
/** Register the callback that handles llm_query() calls from Python. */
|
|
49
|
+
setLlmQueryHandler(handler: LlmQueryHandler): void;
|
|
50
|
+
/** Register the callback that handles thread() calls from Python. */
|
|
51
|
+
setThreadHandler(handler: ThreadHandler): void;
|
|
52
|
+
/** Register the callback that handles merge_threads() calls from Python. */
|
|
53
|
+
setMergeHandler(handler: MergeHandler): void;
|
|
54
|
+
/** Inject the full context string into the Python REPL. */
|
|
55
|
+
setContext(text: string): Promise<void>;
|
|
56
|
+
/** Reset the Final sentinel variable. */
|
|
57
|
+
resetFinal(): Promise<void>;
|
|
58
|
+
/** Execute a code snippet and return the result. */
|
|
59
|
+
execute(code: string): Promise<ExecResult>;
|
|
60
|
+
/** Gracefully shut down the Python subprocess. */
|
|
61
|
+
shutdown(): void;
|
|
62
|
+
private send;
|
|
63
|
+
private handleLine;
|
|
64
|
+
private handleLlmQueryMessage;
|
|
65
|
+
private handleThreadRequestMessage;
|
|
66
|
+
private handleMergeRequestMessage;
|
|
67
|
+
private waitForMessage;
|
|
68
|
+
private cleanup;
|
|
69
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Python REPL manager for swarm-code.
|
|
3
|
+
*
|
|
4
|
+
* Spawns a single Python subprocess running `runtime.py` and keeps it alive
|
|
5
|
+
* across multiple RLM iterations. Communication uses line-delimited JSON
|
|
6
|
+
* over stdin/stdout.
|
|
7
|
+
*
|
|
8
|
+
* Extended from rlm-cli with thread_request and merge_request handlers.
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import * as readline from "node:readline";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
/**
|
|
16
|
+
* Verify Python 3 is available. Throws a clear error if not found.
|
|
17
|
+
*/
|
|
18
|
+
function ensurePython(cmd) {
|
|
19
|
+
try {
|
|
20
|
+
const version = execFileSync(cmd, ["--version"], {
|
|
21
|
+
encoding: "utf-8",
|
|
22
|
+
timeout: 5000,
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
}).trim();
|
|
25
|
+
const major = Number.parseInt(version.replace(/^Python\s*/, "").split(".")[0], 10);
|
|
26
|
+
if (major < 3) {
|
|
27
|
+
throw new Error(`swarm-code requires Python 3 but found ${version}. Install Python 3.x from https://python.org`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (err.code === "ENOENT") {
|
|
32
|
+
throw new Error(`swarm-code requires Python 3 but "${cmd}" was not found on PATH. Install Python 3.x from https://python.org`);
|
|
33
|
+
}
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ── REPL class ──────────────────────────────────────────────────────────────
|
|
38
|
+
export class PythonRepl {
|
|
39
|
+
proc = null;
|
|
40
|
+
rl = null;
|
|
41
|
+
llmQueryHandler = null;
|
|
42
|
+
threadHandler = null;
|
|
43
|
+
mergeHandler = null;
|
|
44
|
+
/**
|
|
45
|
+
* Pending resolvers for messages we're waiting on from Python.
|
|
46
|
+
* Each entry maps a message type to a one-shot resolve/reject pair.
|
|
47
|
+
*/
|
|
48
|
+
pending = new Map();
|
|
49
|
+
/** Whether the REPL subprocess is alive. */
|
|
50
|
+
get isAlive() {
|
|
51
|
+
return this.proc !== null && this.proc.exitCode === null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Start the Python subprocess and wait for it to signal readiness.
|
|
55
|
+
*/
|
|
56
|
+
async start(signal) {
|
|
57
|
+
if (this.isAlive)
|
|
58
|
+
return;
|
|
59
|
+
const runtimePath = path.join(path.dirname(fileURLToPath(import.meta.url)), "runtime.py");
|
|
60
|
+
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
61
|
+
ensurePython(pythonCmd);
|
|
62
|
+
const homeDir = os.homedir();
|
|
63
|
+
this.proc = spawn(pythonCmd, [runtimePath], {
|
|
64
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
65
|
+
env: {
|
|
66
|
+
// Only pass what Python actually needs — not API keys or secrets
|
|
67
|
+
PATH: process.env.PATH,
|
|
68
|
+
HOME: homeDir,
|
|
69
|
+
USERPROFILE: homeDir, // Windows uses USERPROFILE
|
|
70
|
+
PYTHONUNBUFFERED: "1",
|
|
71
|
+
// Windows needs SystemRoot/SYSTEMROOT for Python to find DLLs
|
|
72
|
+
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
|
|
73
|
+
...(process.env.SYSTEMROOT ? { SYSTEMROOT: process.env.SYSTEMROOT } : {}),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
this.rl = readline.createInterface({ input: this.proc.stdout });
|
|
77
|
+
this.rl.on("line", (line) => this.handleLine(line));
|
|
78
|
+
this.proc.stderr.on("data", (chunk) => {
|
|
79
|
+
const text = chunk.toString();
|
|
80
|
+
if (text.trim()) {
|
|
81
|
+
process.stderr.write(`[swarm-repl-python] ${text}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
this.proc.on("close", () => {
|
|
85
|
+
this.cleanup();
|
|
86
|
+
});
|
|
87
|
+
if (signal) {
|
|
88
|
+
signal.addEventListener("abort", () => {
|
|
89
|
+
this.shutdown();
|
|
90
|
+
}, { once: true });
|
|
91
|
+
}
|
|
92
|
+
await this.waitForMessage("ready");
|
|
93
|
+
}
|
|
94
|
+
/** Register the callback that handles llm_query() calls from Python. */
|
|
95
|
+
setLlmQueryHandler(handler) {
|
|
96
|
+
this.llmQueryHandler = handler;
|
|
97
|
+
}
|
|
98
|
+
/** Register the callback that handles thread() calls from Python. */
|
|
99
|
+
setThreadHandler(handler) {
|
|
100
|
+
this.threadHandler = handler;
|
|
101
|
+
}
|
|
102
|
+
/** Register the callback that handles merge_threads() calls from Python. */
|
|
103
|
+
setMergeHandler(handler) {
|
|
104
|
+
this.mergeHandler = handler;
|
|
105
|
+
}
|
|
106
|
+
/** Inject the full context string into the Python REPL. */
|
|
107
|
+
async setContext(text) {
|
|
108
|
+
this.send({ type: "set_context", value: text });
|
|
109
|
+
await this.waitForMessage("context_set");
|
|
110
|
+
}
|
|
111
|
+
/** Reset the Final sentinel variable. */
|
|
112
|
+
async resetFinal() {
|
|
113
|
+
this.send({ type: "reset_final" });
|
|
114
|
+
await this.waitForMessage("final_reset");
|
|
115
|
+
}
|
|
116
|
+
/** Execute a code snippet and return the result. */
|
|
117
|
+
async execute(code) {
|
|
118
|
+
this.send({ type: "exec", code });
|
|
119
|
+
const msg = (await this.waitForMessage("exec_done"));
|
|
120
|
+
return {
|
|
121
|
+
stdout: msg.stdout,
|
|
122
|
+
stderr: msg.stderr,
|
|
123
|
+
hasFinal: msg.has_final,
|
|
124
|
+
finalValue: msg.final_value,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Gracefully shut down the Python subprocess. */
|
|
128
|
+
shutdown() {
|
|
129
|
+
if (this.proc && this.proc.exitCode === null) {
|
|
130
|
+
const proc = this.proc;
|
|
131
|
+
try {
|
|
132
|
+
this.send({ type: "shutdown" });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// stdin may already be closed
|
|
136
|
+
}
|
|
137
|
+
if (process.platform === "win32") {
|
|
138
|
+
// Windows: SIGTERM is ignored, kill immediately
|
|
139
|
+
try {
|
|
140
|
+
proc.kill("SIGKILL");
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* already dead */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// Unix: give Python 500ms to exit gracefully, then force kill
|
|
148
|
+
const killTimer = setTimeout(() => {
|
|
149
|
+
try {
|
|
150
|
+
if (proc.exitCode === null)
|
|
151
|
+
proc.kill("SIGKILL");
|
|
152
|
+
}
|
|
153
|
+
catch { }
|
|
154
|
+
}, 500);
|
|
155
|
+
proc.on("exit", () => clearTimeout(killTimer));
|
|
156
|
+
try {
|
|
157
|
+
proc.kill("SIGTERM");
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
/* already dead */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
this.cleanup();
|
|
165
|
+
}
|
|
166
|
+
// ── Internal ─────────────────────────────────────────────────────────────
|
|
167
|
+
send(msg) {
|
|
168
|
+
if (!this.proc || !this.proc.stdin || this.proc.stdin.destroyed) {
|
|
169
|
+
throw new Error("REPL subprocess is not running");
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
this.proc.stdin.write(`${JSON.stringify(msg)}\n`);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
throw new Error("REPL subprocess stdin write failed");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
handleLine(line) {
|
|
179
|
+
const trimmed = line.trim();
|
|
180
|
+
if (!trimmed)
|
|
181
|
+
return;
|
|
182
|
+
let msg;
|
|
183
|
+
try {
|
|
184
|
+
msg = JSON.parse(trimmed);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Handle async message types (don't go through pending)
|
|
190
|
+
if (msg.type === "llm_query") {
|
|
191
|
+
this.handleLlmQueryMessage(msg).catch((err) => {
|
|
192
|
+
process.stderr.write(`[swarm] llm_query handler error: ${err?.message || err}\n`);
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (msg.type === "thread_request") {
|
|
197
|
+
this.handleThreadRequestMessage(msg).catch((err) => {
|
|
198
|
+
process.stderr.write(`[swarm] thread_request handler error: ${err?.message || err}\n`);
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (msg.type === "merge_request") {
|
|
203
|
+
this.handleMergeRequestMessage(msg).catch((err) => {
|
|
204
|
+
process.stderr.write(`[swarm] merge_request handler error: ${err?.message || err}\n`);
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const entry = this.pending.get(msg.type);
|
|
209
|
+
if (entry) {
|
|
210
|
+
this.pending.delete(msg.type);
|
|
211
|
+
entry.resolve(msg);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async handleLlmQueryMessage(msg) {
|
|
215
|
+
if (!this.llmQueryHandler) {
|
|
216
|
+
this.send({
|
|
217
|
+
type: "llm_result",
|
|
218
|
+
id: msg.id,
|
|
219
|
+
result: "[ERROR] No LLM query handler registered",
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const result = await this.llmQueryHandler(msg.sub_context, msg.instruction);
|
|
225
|
+
this.send({ type: "llm_result", id: msg.id, result });
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
const errorText = err instanceof Error ? err.message : String(err);
|
|
229
|
+
this.send({
|
|
230
|
+
type: "llm_result",
|
|
231
|
+
id: msg.id,
|
|
232
|
+
result: `[ERROR] LLM query failed: ${errorText}`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async handleThreadRequestMessage(msg) {
|
|
237
|
+
if (!this.threadHandler) {
|
|
238
|
+
this.send({
|
|
239
|
+
type: "thread_result",
|
|
240
|
+
id: msg.id,
|
|
241
|
+
result: "[ERROR] No thread handler registered. Run in swarm mode to use thread().",
|
|
242
|
+
success: false,
|
|
243
|
+
files_changed: [],
|
|
244
|
+
duration_ms: 0,
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const result = await this.threadHandler(msg.task, msg.context, msg.agent_backend, msg.model, msg.files || []);
|
|
250
|
+
this.send({
|
|
251
|
+
type: "thread_result",
|
|
252
|
+
id: msg.id,
|
|
253
|
+
result: result.result,
|
|
254
|
+
success: result.success,
|
|
255
|
+
files_changed: result.filesChanged,
|
|
256
|
+
duration_ms: result.durationMs,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
const errorText = err instanceof Error ? err.message : String(err);
|
|
261
|
+
this.send({
|
|
262
|
+
type: "thread_result",
|
|
263
|
+
id: msg.id,
|
|
264
|
+
result: `[ERROR] Thread failed: ${errorText}`,
|
|
265
|
+
success: false,
|
|
266
|
+
files_changed: [],
|
|
267
|
+
duration_ms: 0,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async handleMergeRequestMessage(msg) {
|
|
272
|
+
if (!this.mergeHandler) {
|
|
273
|
+
this.send({
|
|
274
|
+
type: "merge_result",
|
|
275
|
+
id: msg.id,
|
|
276
|
+
result: "[ERROR] No merge handler registered. Run in swarm mode to use merge_threads().",
|
|
277
|
+
success: false,
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const result = await this.mergeHandler();
|
|
283
|
+
this.send({
|
|
284
|
+
type: "merge_result",
|
|
285
|
+
id: msg.id,
|
|
286
|
+
result: result.result,
|
|
287
|
+
success: result.success,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
const errorText = err instanceof Error ? err.message : String(err);
|
|
292
|
+
this.send({
|
|
293
|
+
type: "merge_result",
|
|
294
|
+
id: msg.id,
|
|
295
|
+
result: `[ERROR] Merge failed: ${errorText}`,
|
|
296
|
+
success: false,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
waitForMessage(type) {
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
if (!this.isAlive) {
|
|
303
|
+
reject(new Error(`REPL subprocess is not running (waiting for "${type}")`));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const timeout = setTimeout(() => {
|
|
307
|
+
if (this.pending.has(type)) {
|
|
308
|
+
this.pending.delete(type);
|
|
309
|
+
reject(new Error(`Timeout waiting for "${type}" from Python REPL`));
|
|
310
|
+
}
|
|
311
|
+
}, 300_000);
|
|
312
|
+
this.pending.set(type, {
|
|
313
|
+
resolve: (msg) => {
|
|
314
|
+
clearTimeout(timeout);
|
|
315
|
+
resolve(msg);
|
|
316
|
+
},
|
|
317
|
+
reject: (err) => {
|
|
318
|
+
clearTimeout(timeout);
|
|
319
|
+
reject(err);
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
cleanup() {
|
|
325
|
+
this.rl?.close();
|
|
326
|
+
this.rl = null;
|
|
327
|
+
this.proc = null;
|
|
328
|
+
// Reject all pending promises so callers unblock immediately
|
|
329
|
+
const abortError = new Error("REPL shut down");
|
|
330
|
+
for (const [, entry] of this.pending) {
|
|
331
|
+
entry.reject(abortError);
|
|
332
|
+
}
|
|
333
|
+
this.pending.clear();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
//# sourceMappingURL=repl.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
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 { type Api, type Model } from "@mariozechner/pi-ai";
|
|
15
|
+
import type { MergeHandler, PythonRepl, ThreadHandler } from "./repl.js";
|
|
16
|
+
export interface RlmOptions {
|
|
17
|
+
context: string;
|
|
18
|
+
query: string;
|
|
19
|
+
model: Model<Api>;
|
|
20
|
+
repl: PythonRepl;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
onProgress?: (info: RlmProgress) => void;
|
|
23
|
+
onSubQueryStart?: (info: SubQueryStartInfo) => void;
|
|
24
|
+
onSubQuery?: (info: SubQueryInfo) => void;
|
|
25
|
+
/** Custom system prompt (used for swarm mode) */
|
|
26
|
+
systemPrompt?: string;
|
|
27
|
+
/** Thread handler for swarm mode */
|
|
28
|
+
threadHandler?: ThreadHandler;
|
|
29
|
+
/** Merge handler for swarm mode */
|
|
30
|
+
mergeHandler?: MergeHandler;
|
|
31
|
+
}
|
|
32
|
+
export interface RlmProgress {
|
|
33
|
+
iteration: number;
|
|
34
|
+
maxIterations: number;
|
|
35
|
+
subQueries: number;
|
|
36
|
+
phase: "generating_code" | "executing" | "checking_final";
|
|
37
|
+
code?: string;
|
|
38
|
+
stdout?: string;
|
|
39
|
+
stderr?: string;
|
|
40
|
+
userMessage?: string;
|
|
41
|
+
rawResponse?: string;
|
|
42
|
+
systemPrompt?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface SubQueryStartInfo {
|
|
45
|
+
index: number;
|
|
46
|
+
contextLength: number;
|
|
47
|
+
instruction: string;
|
|
48
|
+
}
|
|
49
|
+
export interface SubQueryInfo {
|
|
50
|
+
index: number;
|
|
51
|
+
contextLength: number;
|
|
52
|
+
instruction: string;
|
|
53
|
+
resultLength: number;
|
|
54
|
+
resultPreview: string;
|
|
55
|
+
elapsedMs: number;
|
|
56
|
+
}
|
|
57
|
+
export interface RlmResult {
|
|
58
|
+
answer: string;
|
|
59
|
+
iterations: number;
|
|
60
|
+
totalSubQueries: number;
|
|
61
|
+
completed: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare function runRlmLoop(options: RlmOptions): Promise<RlmResult>;
|