swarm-code 0.1.17 → 0.1.19
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/dist/core/rlm.js +32 -1
- package/dist/prompts/orchestrator.js +3 -2
- package/dist/ui/text-input.d.ts +12 -18
- package/dist/ui/text-input.js +117 -75
- package/package.json +1 -1
package/dist/core/rlm.js
CHANGED
|
@@ -373,6 +373,15 @@ export async function runRlmLoop(options) {
|
|
|
373
373
|
stderr: execResult.stderr,
|
|
374
374
|
});
|
|
375
375
|
if (execResult.hasFinal && execResult.finalValue !== null) {
|
|
376
|
+
// Auto-merge any unmerged thread branches before returning
|
|
377
|
+
if (mergeHandler) {
|
|
378
|
+
try {
|
|
379
|
+
await mergeHandler();
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// Non-fatal — merge may have already been done by the LLM
|
|
383
|
+
}
|
|
384
|
+
}
|
|
376
385
|
return {
|
|
377
386
|
answer: execResult.finalValue,
|
|
378
387
|
iterations: iteration,
|
|
@@ -392,13 +401,35 @@ export async function runRlmLoop(options) {
|
|
|
392
401
|
parts.push("(No output produced. The code ran without printing anything.)");
|
|
393
402
|
}
|
|
394
403
|
parts.push(`\nIteration ${iteration}/${config.max_iterations}. Sub-queries used: ${totalSubQueries}/${config.max_sub_queries}.`);
|
|
395
|
-
|
|
404
|
+
const remaining = config.max_iterations - iteration;
|
|
405
|
+
if (remaining <= 2) {
|
|
406
|
+
parts.push("⚠️ CRITICAL: Only " +
|
|
407
|
+
remaining +
|
|
408
|
+
" iteration(s) remaining! Call merge_threads() then FINAL() NOW with your best result. Any unmerged work will be lost.");
|
|
409
|
+
}
|
|
410
|
+
else if (remaining <= Math.ceil(config.max_iterations * 0.25)) {
|
|
411
|
+
parts.push("⚠️ WARNING: " +
|
|
412
|
+
remaining +
|
|
413
|
+
" iterations remaining. Wrap up: merge_threads() → FINAL(). Don't start new threads.");
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
parts.push("Continue processing or call FINAL() when you have the answer.");
|
|
417
|
+
}
|
|
396
418
|
conversationHistory.push({
|
|
397
419
|
role: "user",
|
|
398
420
|
content: parts.join("\n\n"),
|
|
399
421
|
timestamp: Date.now(),
|
|
400
422
|
});
|
|
401
423
|
}
|
|
424
|
+
// Auto-merge any remaining thread branches even though FINAL was never called
|
|
425
|
+
if (mergeHandler) {
|
|
426
|
+
try {
|
|
427
|
+
await mergeHandler();
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
// Non-fatal
|
|
431
|
+
}
|
|
432
|
+
}
|
|
402
433
|
return {
|
|
403
434
|
answer: "[Maximum iterations reached without calling FINAL]",
|
|
404
435
|
iterations: config.max_iterations,
|
|
@@ -77,9 +77,10 @@ ${agentDescriptions}
|
|
|
77
77
|
4. Use \`print()\` for intermediate output visible in the next iteration
|
|
78
78
|
5. Max ${config.max_threads} concurrent threads, ${config.max_total_threads} total per session
|
|
79
79
|
6. Thread timeout: ${config.thread_timeout_ms / 1000}s per thread
|
|
80
|
-
7.
|
|
80
|
+
7. After merging, try to run a quick verification thread if iterations allow. But don't endlessly loop on verification — one attempt is enough.
|
|
81
81
|
8. Prefer cheap models for sub-agent threads (haiku, gpt-4o-mini) — save premium models for complex work
|
|
82
|
-
|
|
82
|
+
9. The REPL persists state — variables survive across iterations
|
|
83
|
+
10. **Watch your iteration count.** If you're past 75% of max iterations, call \`merge_threads()\` then \`FINAL()\` with your best result. Don't waste iterations on repeated verification cycles.
|
|
83
84
|
|
|
84
85
|
## Examples
|
|
85
86
|
|
package/dist/ui/text-input.d.ts
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Bordered text input for the interactive REPL.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
4
|
+
* Visual style inspired by OpenCode — full-width bordered text area with
|
|
5
|
+
* a darker background row, accent-colored bottom border, and multi-line
|
|
6
|
+
* paste support.
|
|
7
|
+
*
|
|
8
|
+
* Keys:
|
|
9
|
+
* - Enter: submit
|
|
10
|
+
* - Escape / Ctrl+C: exit
|
|
11
|
+
* - Ctrl+D: submit
|
|
12
|
+
* - Backspace, left/right, home/end: editing
|
|
13
|
+
* - Paste (multi-char with newlines): captured as multi-line
|
|
11
14
|
*/
|
|
12
15
|
export interface TextInputResult {
|
|
13
16
|
text: string;
|
|
14
17
|
action: "submit" | "escape";
|
|
15
18
|
}
|
|
16
|
-
|
|
17
|
-
* Read user input with multi-line paste support and escape-to-exit.
|
|
18
|
-
*
|
|
19
|
-
* Behavior:
|
|
20
|
-
* - Single Enter: submits current line(s)
|
|
21
|
-
* - Pasted text with newlines: captured as multi-line, then Enter submits all
|
|
22
|
-
* - Escape: returns action "escape" to signal exit
|
|
23
|
-
* - Ctrl+D: submits whatever is in the buffer
|
|
24
|
-
*/
|
|
25
|
-
export declare function readTextInput(prompt: string): Promise<TextInputResult>;
|
|
19
|
+
export declare function readTextInput(_prompt: string): Promise<TextInputResult>;
|
package/dist/ui/text-input.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Bordered text input for the interactive REPL.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - Escape → signals exit
|
|
8
|
-
* - Ctrl+D → submits current input
|
|
9
|
-
* - Ctrl+C → signals exit
|
|
10
|
-
* - Standard editing: backspace, left/right arrows, home/end
|
|
11
|
-
*/
|
|
12
|
-
import { dim, isTTY, stripAnsi } from "./theme.js";
|
|
13
|
-
/**
|
|
14
|
-
* Read user input with multi-line paste support and escape-to-exit.
|
|
4
|
+
* Visual style inspired by OpenCode — full-width bordered text area with
|
|
5
|
+
* a darker background row, accent-colored bottom border, and multi-line
|
|
6
|
+
* paste support.
|
|
15
7
|
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
20
|
-
* -
|
|
8
|
+
* Keys:
|
|
9
|
+
* - Enter: submit
|
|
10
|
+
* - Escape / Ctrl+C: exit
|
|
11
|
+
* - Ctrl+D: submit
|
|
12
|
+
* - Backspace, left/right, home/end: editing
|
|
13
|
+
* - Paste (multi-char with newlines): captured as multi-line
|
|
21
14
|
*/
|
|
22
|
-
|
|
15
|
+
import { coral, dim, isTTY, termWidth } from "./theme.js";
|
|
16
|
+
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
17
|
+
const BG_DARK = "\x1b[48;2;30;33;39m"; // dark background for input row
|
|
18
|
+
const RESET = "\x1b[0m";
|
|
19
|
+
const HIDE_CURSOR = "\x1b[?25l";
|
|
20
|
+
const SHOW_CURSOR = "\x1b[?25h";
|
|
21
|
+
const BORDER_COLOR = "\x1b[38;2;60;63;68m"; // subtle gray for top border
|
|
22
|
+
const ACCENT_COLOR = "\x1b[38;2;215;119;87m"; // coral for bottom border
|
|
23
|
+
export function readTextInput(_prompt) {
|
|
23
24
|
if (!isTTY) {
|
|
24
25
|
return new Promise((resolve) => {
|
|
25
26
|
let data = "";
|
|
@@ -37,124 +38,138 @@ export function readTextInput(prompt) {
|
|
|
37
38
|
}
|
|
38
39
|
return new Promise((resolve) => {
|
|
39
40
|
const linesBuf = [""];
|
|
40
|
-
let cursorPos = 0;
|
|
41
|
+
let cursorPos = 0;
|
|
41
42
|
const origRawMode = process.stdin.isRaw;
|
|
43
|
+
const w = termWidth();
|
|
42
44
|
process.stdin.setRawMode(true);
|
|
43
45
|
process.stdin.resume();
|
|
44
46
|
process.stdin.setEncoding("utf-8");
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
process.stderr
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
// Track how many terminal rows we've drawn so we can clear them
|
|
48
|
+
let drawnRows = 0;
|
|
49
|
+
function drawBox() {
|
|
50
|
+
const out = process.stderr;
|
|
51
|
+
// Clear previously drawn rows
|
|
52
|
+
if (drawnRows > 0) {
|
|
53
|
+
for (let i = 0; i < drawnRows; i++) {
|
|
54
|
+
out.write("\x1b[1A\x1b[2K");
|
|
55
|
+
}
|
|
54
56
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
// Top border — thin dim line
|
|
58
|
+
const topLine = `${BORDER_COLOR}${"─".repeat(w)}${RESET}`;
|
|
59
|
+
out.write(`${topLine}\n`);
|
|
60
|
+
// Content rows — dark background, full width
|
|
61
|
+
const promptChar = `${ACCENT_COLOR}❯${RESET} `;
|
|
62
|
+
const promptVisibleLen = 2; // "❯ "
|
|
63
|
+
for (let i = 0; i < linesBuf.length; i++) {
|
|
64
|
+
const lineText = linesBuf[i];
|
|
65
|
+
const prefix = i === 0 ? promptChar : `${dim("·")} `;
|
|
66
|
+
const prefixVisibleLen = promptVisibleLen;
|
|
67
|
+
// How much space for text content
|
|
68
|
+
const contentWidth = w - prefixVisibleLen;
|
|
69
|
+
// Truncate display if line is too long
|
|
70
|
+
const displayText = lineText.length > contentWidth ? lineText.slice(0, contentWidth - 1) + "…" : lineText;
|
|
71
|
+
const padding = Math.max(0, contentWidth - displayText.length);
|
|
72
|
+
out.write(`${BG_DARK}${prefix}${displayText}${" ".repeat(padding)}${RESET}\n`);
|
|
61
73
|
}
|
|
74
|
+
// Bottom border — accent colored
|
|
75
|
+
const bottomLine = `${ACCENT_COLOR}${"─".repeat(w)}${RESET}`;
|
|
76
|
+
out.write(`${bottomLine}\n`);
|
|
77
|
+
// Hints
|
|
78
|
+
out.write(`${dim(" enter submit esc exit")}\n`);
|
|
79
|
+
drawnRows = linesBuf.length + 3; // top border + content lines + bottom border + hints
|
|
80
|
+
// Position cursor inside the text area
|
|
81
|
+
// We're at the bottom (after hints). Move up to the correct content row.
|
|
82
|
+
const currentLineIdx = linesBuf.length - 1; // cursor is always on last line
|
|
83
|
+
const rowsFromBottom = 2 + (linesBuf.length - 1 - currentLineIdx); // hints + bottom border + lines below cursor
|
|
84
|
+
out.write(`\x1b[${rowsFromBottom}A`);
|
|
85
|
+
// Move to correct column: prefix width + cursor position
|
|
86
|
+
const col = promptVisibleLen + cursorPos + 1;
|
|
87
|
+
out.write(`\x1b[${col}G`);
|
|
62
88
|
}
|
|
63
|
-
//
|
|
64
|
-
process.stderr.write(
|
|
89
|
+
// Initial draw
|
|
90
|
+
process.stderr.write(HIDE_CURSOR);
|
|
91
|
+
drawBox();
|
|
92
|
+
process.stderr.write(SHOW_CURSOR);
|
|
65
93
|
const onData = (data) => {
|
|
66
|
-
// Check if this looks like a paste (multiple chars with newlines)
|
|
67
94
|
const hasNewlines = data.includes("\n") || data.includes("\r");
|
|
68
95
|
const isMultiChar = data.length > 1;
|
|
69
96
|
const isPaste = hasNewlines && isMultiChar;
|
|
70
97
|
if (isPaste) {
|
|
71
|
-
// Paste mode — split on newlines, add all to buffer
|
|
72
98
|
const pastedLines = data.split(/\r\n|\r|\n/);
|
|
73
|
-
// Append first fragment to current line at cursor
|
|
74
99
|
const currentLine = linesBuf[linesBuf.length - 1];
|
|
75
100
|
linesBuf[linesBuf.length - 1] = currentLine.slice(0, cursorPos) + pastedLines[0] + currentLine.slice(cursorPos);
|
|
76
|
-
// Redraw current line with pasted content
|
|
77
101
|
cursorPos = (currentLine.slice(0, cursorPos) + pastedLines[0]).length;
|
|
78
|
-
redrawCurrentLine();
|
|
79
|
-
// Add remaining lines
|
|
80
102
|
for (let i = 1; i < pastedLines.length; i++) {
|
|
81
103
|
const line = pastedLines[i];
|
|
82
|
-
if (i === pastedLines.length - 1 && line === "")
|
|
83
|
-
// Trailing newline — don't add empty line
|
|
104
|
+
if (i === pastedLines.length - 1 && line === "")
|
|
84
105
|
break;
|
|
85
|
-
}
|
|
86
|
-
process.stderr.write("\n");
|
|
87
106
|
linesBuf.push(line);
|
|
88
107
|
cursorPos = line.length;
|
|
89
|
-
redrawCurrentLine();
|
|
90
108
|
}
|
|
109
|
+
// Move cursor back to top of box before redraw
|
|
110
|
+
moveCursorToBoxTop();
|
|
111
|
+
drawBox();
|
|
91
112
|
return;
|
|
92
113
|
}
|
|
93
|
-
// Character-by-character processing
|
|
94
114
|
for (let i = 0; i < data.length; i++) {
|
|
95
115
|
const ch = data[i];
|
|
96
116
|
// Escape sequences
|
|
97
117
|
if (ch === "\x1b") {
|
|
98
118
|
if (data[i + 1] === "[") {
|
|
99
119
|
const code = data[i + 2];
|
|
100
|
-
if (code === "C") {
|
|
101
|
-
|
|
102
|
-
if (cursorPos < linesBuf[linesBuf.length - 1].length) {
|
|
103
|
-
cursorPos++;
|
|
104
|
-
redrawCurrentLine();
|
|
105
|
-
}
|
|
120
|
+
if (code === "C" && cursorPos < linesBuf[linesBuf.length - 1].length) {
|
|
121
|
+
cursorPos++;
|
|
106
122
|
i += 2;
|
|
123
|
+
moveCursorToBoxTop();
|
|
124
|
+
drawBox();
|
|
107
125
|
continue;
|
|
108
126
|
}
|
|
109
|
-
if (code === "D") {
|
|
110
|
-
|
|
111
|
-
if (cursorPos > 0) {
|
|
112
|
-
cursorPos--;
|
|
113
|
-
redrawCurrentLine();
|
|
114
|
-
}
|
|
127
|
+
if (code === "D" && cursorPos > 0) {
|
|
128
|
+
cursorPos--;
|
|
115
129
|
i += 2;
|
|
130
|
+
moveCursorToBoxTop();
|
|
131
|
+
drawBox();
|
|
116
132
|
continue;
|
|
117
133
|
}
|
|
118
134
|
if (code === "H") {
|
|
119
|
-
// Home
|
|
120
135
|
cursorPos = 0;
|
|
121
|
-
redrawCurrentLine();
|
|
122
136
|
i += 2;
|
|
137
|
+
moveCursorToBoxTop();
|
|
138
|
+
drawBox();
|
|
123
139
|
continue;
|
|
124
140
|
}
|
|
125
141
|
if (code === "F") {
|
|
126
|
-
// End
|
|
127
142
|
cursorPos = linesBuf[linesBuf.length - 1].length;
|
|
128
|
-
redrawCurrentLine();
|
|
129
143
|
i += 2;
|
|
144
|
+
moveCursorToBoxTop();
|
|
145
|
+
drawBox();
|
|
130
146
|
continue;
|
|
131
147
|
}
|
|
132
|
-
// Skip other escape sequences
|
|
133
148
|
i += 2;
|
|
134
149
|
continue;
|
|
135
150
|
}
|
|
136
|
-
// Bare Escape
|
|
137
|
-
|
|
151
|
+
// Bare Escape — exit
|
|
152
|
+
finishAndClear();
|
|
138
153
|
resolve({ text: "", action: "escape" });
|
|
139
154
|
return;
|
|
140
155
|
}
|
|
141
156
|
// Ctrl+D — submit
|
|
142
157
|
if (ch === "\x04") {
|
|
143
158
|
const text = linesBuf.join("\n").trim();
|
|
144
|
-
|
|
159
|
+
finishAndClear();
|
|
145
160
|
resolve({ text, action: "submit" });
|
|
146
161
|
return;
|
|
147
162
|
}
|
|
148
163
|
// Ctrl+C — exit
|
|
149
164
|
if (ch === "\x03") {
|
|
150
|
-
|
|
165
|
+
finishAndClear();
|
|
151
166
|
resolve({ text: "", action: "escape" });
|
|
152
167
|
return;
|
|
153
168
|
}
|
|
154
169
|
// Enter — submit
|
|
155
170
|
if (ch === "\r" || ch === "\n") {
|
|
156
171
|
const text = linesBuf.join("\n").trim();
|
|
157
|
-
|
|
172
|
+
finishAndClear();
|
|
158
173
|
resolve({ text, action: "submit" });
|
|
159
174
|
return;
|
|
160
175
|
}
|
|
@@ -164,7 +179,8 @@ export function readTextInput(prompt) {
|
|
|
164
179
|
const line = linesBuf[linesBuf.length - 1];
|
|
165
180
|
linesBuf[linesBuf.length - 1] = line.slice(0, cursorPos - 1) + line.slice(cursorPos);
|
|
166
181
|
cursorPos--;
|
|
167
|
-
|
|
182
|
+
moveCursorToBoxTop();
|
|
183
|
+
drawBox();
|
|
168
184
|
}
|
|
169
185
|
continue;
|
|
170
186
|
}
|
|
@@ -173,7 +189,8 @@ export function readTextInput(prompt) {
|
|
|
173
189
|
const line = linesBuf[linesBuf.length - 1];
|
|
174
190
|
linesBuf[linesBuf.length - 1] = line.slice(0, cursorPos) + " " + line.slice(cursorPos);
|
|
175
191
|
cursorPos += 2;
|
|
176
|
-
|
|
192
|
+
moveCursorToBoxTop();
|
|
193
|
+
drawBox();
|
|
177
194
|
continue;
|
|
178
195
|
}
|
|
179
196
|
// Regular printable character
|
|
@@ -181,16 +198,41 @@ export function readTextInput(prompt) {
|
|
|
181
198
|
const line = linesBuf[linesBuf.length - 1];
|
|
182
199
|
linesBuf[linesBuf.length - 1] = line.slice(0, cursorPos) + ch + line.slice(cursorPos);
|
|
183
200
|
cursorPos++;
|
|
184
|
-
|
|
201
|
+
moveCursorToBoxTop();
|
|
202
|
+
drawBox();
|
|
185
203
|
}
|
|
186
204
|
}
|
|
187
205
|
};
|
|
188
|
-
function
|
|
206
|
+
function moveCursorToBoxTop() {
|
|
207
|
+
// From current cursor position (inside the text area), move to the line
|
|
208
|
+
// before the top border so drawBox() can clear and redraw from there.
|
|
209
|
+
// Current cursor is at content row (linesBuf.length - 1 from top border)
|
|
210
|
+
// We need to go up past: content rows above cursor + top border
|
|
211
|
+
// But drawBox handles clearing with drawnRows, so just go up to start
|
|
212
|
+
const currentLineIdx = linesBuf.length - 1;
|
|
213
|
+
const rowsUp = currentLineIdx + 1; // content lines above + top border
|
|
214
|
+
if (rowsUp > 0) {
|
|
215
|
+
process.stderr.write(`\x1b[${rowsUp}A`);
|
|
216
|
+
}
|
|
217
|
+
process.stderr.write("\x1b[0G");
|
|
218
|
+
}
|
|
219
|
+
function finishAndClear() {
|
|
189
220
|
process.stdin.removeListener("data", onData);
|
|
190
221
|
if (origRawMode !== undefined) {
|
|
191
222
|
process.stdin.setRawMode(origRawMode);
|
|
192
223
|
}
|
|
193
|
-
|
|
224
|
+
// Move cursor to top of box and clear everything
|
|
225
|
+
moveCursorToBoxTop();
|
|
226
|
+
process.stderr.write("\x1b[J"); // erase to end of screen
|
|
227
|
+
// Write the submitted text as a clean line (so it's visible in scrollback)
|
|
228
|
+
const fullText = linesBuf.join("\n").trim();
|
|
229
|
+
if (fullText) {
|
|
230
|
+
const displayLines = fullText.split("\n");
|
|
231
|
+
for (let i = 0; i < displayLines.length; i++) {
|
|
232
|
+
const prefix = i === 0 ? ` ${coral("swarm")}${dim(">")} ` : ` ${dim(".")} `;
|
|
233
|
+
process.stderr.write(`${prefix}${displayLines[i]}\n`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
194
236
|
}
|
|
195
237
|
process.stdin.on("data", onData);
|
|
196
238
|
});
|
package/package.json
CHANGED