@hicaru/pi-rlm 0.1.9 → 0.2.1
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/package.json +1 -1
- package/src/bridge/fallback-todo.ts +12 -1
- package/src/bridge/subcall-handlers.ts +336 -0
- package/src/commands/rlm-config.ts +8 -8
- package/src/commands/rlm.ts +48 -12
- package/src/config/defaults.ts +4 -1
- package/src/config/settings.ts +33 -3
- package/src/context/repomix-context.ts +5 -10
- package/src/core/answer.ts +4 -3
- package/src/core/artifacts.ts +4 -3
- package/src/core/engine.ts +101 -267
- package/src/core/gates.ts +3 -3
- package/src/core/limits.ts +19 -1
- package/src/core/pipeline-handlers.ts +319 -0
- package/src/core/pipeline.ts +2 -2
- package/src/core/types.ts +25 -27
- package/src/index.ts +63 -17
- package/src/mode/rlm-mode.ts +8 -11
- package/src/prompts/system.ts +164 -52
- package/src/prompts/user.ts +1 -5
- package/src/sandbox/protocol.ts +6 -7
- package/src/sandbox/sandbox-manager.ts +25 -11
- package/src/sandbox/sandbox.ts +93 -22
- package/src/sandbox/worker.py +798 -66
- package/src/state/paths.ts +1 -1
- package/src/state/reads.ts +12 -4
- package/src/state/resume.ts +5 -11
- package/src/text/parsing.ts +0 -6
- package/src/tool/background-tasks.ts +95 -0
- package/src/tool/repl-details.ts +2 -0
- package/src/tool/repl-tool.ts +223 -318
- package/src/tool/rlm-details.ts +0 -10
- package/src/tool/rlm-events.ts +10 -2
- package/src/tool/rlm-tool.ts +18 -31
- package/src/tool/subcall-render.ts +75 -11
- package/src/tool/subcall-store.ts +57 -1
- package/src/ui/config-panel.ts +41 -21
- package/src/ui/intro.ts +2 -1
- package/src/ui/status.ts +8 -5
- package/src/ui/theme-adapter.ts +36 -0
- package/src/ui/theme.ts +0 -25
- package/src/util/concurrency.ts +87 -13
- package/src/util/trace.ts +42 -0
- package/src/bridge/llm-query.ts +0 -133
- package/src/bridge/rlm-query.ts +0 -122
- package/src/mode/input-router.ts +0 -23
package/src/sandbox/sandbox.ts
CHANGED
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
type WorkerRequest,
|
|
25
25
|
type WorkerResponse,
|
|
26
26
|
} from "./protocol.ts";
|
|
27
|
-
import { formatError } from "../util/errors.ts";
|
|
27
|
+
import { errorMessage, formatError } from "../util/errors.ts";
|
|
28
|
+
import { trace, traceEnabled } from "../util/trace.ts";
|
|
28
29
|
|
|
29
30
|
/** Result of a host-side library pack requested by `load_library`. */
|
|
30
31
|
export interface LibraryLoadResult {
|
|
@@ -38,12 +39,23 @@ export interface LibraryLoadResult {
|
|
|
38
39
|
readonly alreadyLoaded?: boolean;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Per-interrupt routing context for the sub-LLM handlers.
|
|
44
|
+
*
|
|
45
|
+
* Only the four sub-call kinds can be spawned, so only they carry it; the interactive and
|
|
46
|
+
* pipeline handlers are always synchronous within one exec.
|
|
47
|
+
*/
|
|
48
|
+
export interface SubcallOpts {
|
|
49
|
+
/** Started via `spawn()` — route to session-scoped state, not the current invocation. */
|
|
50
|
+
readonly detached: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
/** Handlers the bridge installs to service sub-LLM interrupts. Return the reply payload. */
|
|
42
54
|
export interface SubLlmHandlers {
|
|
43
|
-
llmQuery(prompt: string, model: string | null, depth: number): Promise<string>;
|
|
44
|
-
llmQueryBatched(prompts: readonly string[], model: string | null, depth: number): Promise<string[]>;
|
|
45
|
-
rlmQuery(prompt: string, model: string | null, depth: number): Promise<string>;
|
|
46
|
-
rlmQueryBatched(prompts: readonly string[], model: string | null, depth: number): Promise<string[]>;
|
|
55
|
+
llmQuery(prompt: string, model: string | null, depth: number, opts: SubcallOpts): Promise<string>;
|
|
56
|
+
llmQueryBatched(prompts: readonly string[], model: string | null, depth: number, opts: SubcallOpts): Promise<string[]>;
|
|
57
|
+
rlmQuery(prompt: string, model: string | null, depth: number, opts: SubcallOpts): Promise<string>;
|
|
58
|
+
rlmQueryBatched(prompts: readonly string[], model: string | null, depth: number, opts: SubcallOpts): Promise<string[]>;
|
|
47
59
|
advancePhase(phase: string, summary: string | undefined, depth: number): Promise<string>;
|
|
48
60
|
saveArtifact(kind: string, content: string, depth: number): Promise<string>;
|
|
49
61
|
askUserQuestion(questions: readonly AskQuestion[], depth: number): Promise<AskAnswer[]>;
|
|
@@ -73,9 +85,15 @@ export interface SandboxOptions {
|
|
|
73
85
|
* Native repl() data work leaves this false so scratch-file writes still work.
|
|
74
86
|
*/
|
|
75
87
|
readonly readOnly?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Max seconds the worker will wait for a host reply while parked in `_drain_until`
|
|
90
|
+
* (rlm_await / sync sub-call). Defaults to the worker's own RLM_AWAIT_TIMEOUT_S (600).
|
|
91
|
+
*/
|
|
92
|
+
readonly awaitTimeoutS?: number;
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
const WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), "worker.py");
|
|
96
|
+
const STDERR_TAIL_CHARS = 8_192;
|
|
79
97
|
const TODO_PROTO_KEYS = new Set(["type", "rid", "depth", "action"]);
|
|
80
98
|
|
|
81
99
|
// The sandbox runs untrusted model-authored code; it must never inherit provider secrets.
|
|
@@ -124,7 +142,23 @@ export class PythonSandbox {
|
|
|
124
142
|
private readonly handlers: SubLlmHandlers;
|
|
125
143
|
private readonly requestTimeoutMs: number;
|
|
126
144
|
private readonly initTimeoutMs: number;
|
|
127
|
-
|
|
145
|
+
/** Bounded stderr tail (chunks, newest last) — avoids rebuilding the buffer per chunk. */
|
|
146
|
+
private readonly stderrTail: string[] = [];
|
|
147
|
+
private stderrLen = 0;
|
|
148
|
+
|
|
149
|
+
/** Bounded tail of everything written to stderr, oldest chunks already dropped. */
|
|
150
|
+
private get stderr(): string {
|
|
151
|
+
return this.stderrTail.join("");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Record a diagnostic on the same bounded tail as real worker stderr. */
|
|
155
|
+
private appendStderr(text: string): void {
|
|
156
|
+
this.stderrTail.push(text);
|
|
157
|
+
this.stderrLen += text.length;
|
|
158
|
+
while (this.stderrLen > STDERR_TAIL_CHARS && this.stderrTail.length > 1) {
|
|
159
|
+
this.stderrLen -= (this.stderrTail.shift() ?? "").length;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
128
162
|
private disposed = false;
|
|
129
163
|
private ready: Promise<void>;
|
|
130
164
|
|
|
@@ -144,6 +178,9 @@ export class PythonSandbox {
|
|
|
144
178
|
if (opts.readOnly) {
|
|
145
179
|
workerArgs.push("--read-only");
|
|
146
180
|
}
|
|
181
|
+
if (opts.awaitTimeoutS !== undefined) {
|
|
182
|
+
workerArgs.push("--await-timeout", String(opts.awaitTimeoutS));
|
|
183
|
+
}
|
|
147
184
|
this.proc = spawn(
|
|
148
185
|
python,
|
|
149
186
|
workerArgs,
|
|
@@ -153,9 +190,7 @@ export class PythonSandbox {
|
|
|
153
190
|
this.proc.stdout.setEncoding("utf8");
|
|
154
191
|
this.proc.stdout.on("data", (chunk: string) => this.onData(chunk));
|
|
155
192
|
this.proc.stderr.setEncoding("utf8");
|
|
156
|
-
this.proc.stderr.on("data", (chunk: string) =>
|
|
157
|
-
this.stderr = (this.stderr + chunk).slice(-8192);
|
|
158
|
-
});
|
|
193
|
+
this.proc.stderr.on("data", (chunk: string) => this.appendStderr(chunk));
|
|
159
194
|
this.proc.on("error", (err: NodeJS.ErrnoException) => {
|
|
160
195
|
const hint = err.code === "ENOENT" ? ` ('${python}' not found — is Python installed and on PATH?)` : "";
|
|
161
196
|
this.failAll(new Error(`failed to start sandbox${hint}: ${err.message}`));
|
|
@@ -189,12 +224,12 @@ export class PythonSandbox {
|
|
|
189
224
|
return sandbox;
|
|
190
225
|
}
|
|
191
226
|
|
|
192
|
-
async loadContext(payload: unknown
|
|
227
|
+
async loadContext(payload: unknown): Promise<number> {
|
|
193
228
|
const isJson = typeof payload !== "string";
|
|
194
229
|
let path: string | undefined;
|
|
195
230
|
try {
|
|
196
231
|
path = await this.writeContextFile(payload, isJson);
|
|
197
|
-
const res = await this.request({ type: "load_context", path,
|
|
232
|
+
const res = await this.request({ type: "load_context", path, json: isJson });
|
|
198
233
|
if (!res.ok) throw new Error(res.error ?? "load_context failed");
|
|
199
234
|
return res.index ?? 0;
|
|
200
235
|
} finally {
|
|
@@ -216,8 +251,8 @@ export class PythonSandbox {
|
|
|
216
251
|
return file;
|
|
217
252
|
}
|
|
218
253
|
|
|
219
|
-
async exec(code: string): Promise<ReplResult> {
|
|
220
|
-
const res = await this.request({ type: "exec", code });
|
|
254
|
+
async exec(code: string, signal?: AbortSignal): Promise<ReplResult> {
|
|
255
|
+
const res = await this.request({ type: "exec", code }, signal);
|
|
221
256
|
if (!res.ok) throw new Error(res.error ?? "exec failed");
|
|
222
257
|
return {
|
|
223
258
|
stdout: res.stdout ?? "",
|
|
@@ -282,12 +317,27 @@ export class PythonSandbox {
|
|
|
282
317
|
});
|
|
283
318
|
}
|
|
284
319
|
|
|
285
|
-
private request(payload: RequestBody): Promise<WorkerResponse> {
|
|
320
|
+
private request(payload: RequestBody, signal?: AbortSignal): Promise<WorkerResponse> {
|
|
286
321
|
if (this.disposed) return Promise.reject(new Error("sandbox disposed"));
|
|
322
|
+
if (signal?.aborted) return Promise.reject(new Error("repl execution aborted"));
|
|
287
323
|
const id = `r${++this.seq}`;
|
|
288
324
|
return new Promise<WorkerResponse>((resolve, reject) => {
|
|
289
325
|
const timer = this.createWatchdog(id, payload.type, reject);
|
|
290
|
-
|
|
326
|
+
// Cancel == kill. The worker may be parked inside `_drain_until` with no other way out;
|
|
327
|
+
// `proc.on("exit") -> failAll` settles this request and SandboxManager's catch recreates
|
|
328
|
+
// the sandbox. REPL variables are lost — the documented price of interrupting.
|
|
329
|
+
const onAbort = (): void => {
|
|
330
|
+
this.pending.delete(id);
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
try { this.proc.kill("SIGKILL"); } catch { /* already dead */ }
|
|
333
|
+
reject(new Error("repl execution aborted — REPL variables were reset"));
|
|
334
|
+
};
|
|
335
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
336
|
+
const once = <T>(settle: (value: T) => void) => (value: T): void => {
|
|
337
|
+
signal?.removeEventListener("abort", onAbort);
|
|
338
|
+
settle(value);
|
|
339
|
+
};
|
|
340
|
+
this.pending.set(id, { resolve: once(resolve), reject: once(reject), timer, requestType: payload.type });
|
|
291
341
|
this.send({ id, ...payload } as ParentMessage);
|
|
292
342
|
});
|
|
293
343
|
}
|
|
@@ -318,6 +368,13 @@ export class PythonSandbox {
|
|
|
318
368
|
}
|
|
319
369
|
|
|
320
370
|
private send(msg: ParentMessage): void {
|
|
371
|
+
if (traceEnabled) {
|
|
372
|
+
trace("frame.out", {
|
|
373
|
+
frame: msg.type,
|
|
374
|
+
id: "id" in msg ? msg.id : undefined,
|
|
375
|
+
rid: "rid" in msg ? msg.rid : undefined,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
321
378
|
this.proc.stdin.write(`${JSON.stringify(msg)}\n`);
|
|
322
379
|
}
|
|
323
380
|
|
|
@@ -331,11 +388,11 @@ export class PythonSandbox {
|
|
|
331
388
|
try {
|
|
332
389
|
const message = JSON.parse(line) as unknown;
|
|
333
390
|
if (isWorkerMessage(message)) this.dispatch(message);
|
|
334
|
-
else this.
|
|
391
|
+
else this.appendStderr(`\n[protocol] skipped invalid stdout message: ${line.slice(0, 200)}`);
|
|
335
392
|
} catch {
|
|
336
393
|
// Non-JSON line on the protocol stream — likely a subprocess writing to fd 1.
|
|
337
394
|
// Skip it so a rogue write doesn't kill the pump, but retain a breadcrumb for watchdog errors.
|
|
338
|
-
this.
|
|
395
|
+
this.appendStderr(`\n[protocol] skipped non-JSON stdout line: ${line.slice(0, 200)}`);
|
|
339
396
|
}
|
|
340
397
|
}
|
|
341
398
|
}
|
|
@@ -347,6 +404,19 @@ export class PythonSandbox {
|
|
|
347
404
|
}
|
|
348
405
|
|
|
349
406
|
private dispatch(msg: WorkerMessage): void {
|
|
407
|
+
if (traceEnabled) {
|
|
408
|
+
if (isInterrupt(msg)) {
|
|
409
|
+
trace("frame.in", {
|
|
410
|
+
frame: msg.type,
|
|
411
|
+
rid: msg.rid,
|
|
412
|
+
depth: msg.depth,
|
|
413
|
+
detached: msg.detached === true,
|
|
414
|
+
prompts: "prompts" in msg ? msg.prompts?.length : 1,
|
|
415
|
+
});
|
|
416
|
+
} else {
|
|
417
|
+
trace("frame.in", { frame: "response", id: msg.id, ok: msg.ok });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
350
420
|
if (isInterrupt(msg)) {
|
|
351
421
|
this.touchPending();
|
|
352
422
|
void this.serviceInterrupt(msg);
|
|
@@ -362,18 +432,19 @@ export class PythonSandbox {
|
|
|
362
432
|
private async serviceInterrupt(msg: WorkerInterrupt): Promise<void> {
|
|
363
433
|
const h = this.handlers;
|
|
364
434
|
const d = msg.depth;
|
|
435
|
+
const opts: SubcallOpts = { detached: msg.detached === true };
|
|
365
436
|
try {
|
|
366
437
|
if (msg.type === "llm_query") {
|
|
367
|
-
const response = await h.llmQuery(msg.prompt ?? "", msg.model ?? null, d);
|
|
438
|
+
const response = await h.llmQuery(msg.prompt ?? "", msg.model ?? null, d, opts);
|
|
368
439
|
this.reply(msg.rid, { response });
|
|
369
440
|
} else if (msg.type === "rlm_query") {
|
|
370
|
-
const response = await h.rlmQuery(msg.prompt ?? "", msg.model ?? null, d);
|
|
441
|
+
const response = await h.rlmQuery(msg.prompt ?? "", msg.model ?? null, d, opts);
|
|
371
442
|
this.reply(msg.rid, { response });
|
|
372
443
|
} else if (msg.type === "llm_query_batched") {
|
|
373
|
-
const responses = await h.llmQueryBatched(msg.prompts ?? [], msg.model ?? null, d);
|
|
444
|
+
const responses = await h.llmQueryBatched(msg.prompts ?? [], msg.model ?? null, d, opts);
|
|
374
445
|
this.reply(msg.rid, { responses });
|
|
375
446
|
} else if (msg.type === "rlm_query_batched") {
|
|
376
|
-
const responses = await h.rlmQueryBatched(msg.prompts ?? [], msg.model ?? null, d);
|
|
447
|
+
const responses = await h.rlmQueryBatched(msg.prompts ?? [], msg.model ?? null, d, opts);
|
|
377
448
|
this.reply(msg.rid, { responses });
|
|
378
449
|
} else if (msg.type === "advance_phase") {
|
|
379
450
|
const response = await h.advancePhase(msg.phase ?? "", msg.summary, d);
|
|
@@ -419,7 +490,7 @@ export class PythonSandbox {
|
|
|
419
490
|
}
|
|
420
491
|
}
|
|
421
492
|
} catch (err) {
|
|
422
|
-
this.reply(msg.rid, { error:
|
|
493
|
+
this.reply(msg.rid, { error: errorMessage(err) });
|
|
423
494
|
}
|
|
424
495
|
}
|
|
425
496
|
|