@hicaru/pi-rlm 0.2.0 → 0.2.2
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/README.md +12 -35
- package/README.ru.md +18 -23
- package/README.zh-CN.md +17 -28
- package/package.json +1 -1
- package/src/bridge/library.ts +61 -26
- package/src/bridge/subcall-handlers.ts +382 -0
- package/src/commands/rlm-config.ts +47 -18
- package/src/commands/rlm.ts +3 -152
- package/src/config/defaults.ts +7 -15
- package/src/config/settings.ts +8 -32
- package/src/context/library-context.ts +90 -17
- package/src/core/engine.ts +115 -360
- package/src/core/history.ts +1 -1
- package/src/core/limits.ts +5 -12
- package/src/core/resource-limits.ts +0 -2
- package/src/core/types.ts +3 -36
- package/src/index.ts +49 -10
- package/src/mode/llm-model.ts +54 -0
- package/src/mode/rlm-mode.ts +26 -57
- package/src/prompts/glossary.ts +287 -0
- package/src/prompts/native.ts +127 -0
- package/src/prompts/system.ts +14 -386
- package/src/sandbox/context-file.ts +154 -0
- package/src/sandbox/interrupts.ts +145 -0
- package/src/sandbox/protocol.ts +14 -69
- package/src/sandbox/py/guards.py +150 -0
- package/src/sandbox/py/retrieval.py +265 -0
- package/src/sandbox/py/tasks.py +116 -0
- package/src/sandbox/py/worker.py +836 -0
- package/src/sandbox/sandbox-manager.ts +33 -6
- package/src/sandbox/sandbox.ts +153 -182
- package/src/text/tokens.ts +29 -3
- package/src/tool/background-tasks.ts +95 -0
- package/src/tool/repl-details.ts +4 -2
- package/src/tool/repl-render.ts +58 -0
- package/src/tool/repl-result.ts +70 -0
- package/src/tool/repl-tool.ts +178 -216
- package/src/tool/rlm-aggregator.ts +2 -10
- package/src/tool/rlm-details.ts +0 -2
- package/src/tool/rlm-events.ts +10 -16
- package/src/tool/rlm-tool.ts +1 -12
- package/src/tool/subcall-render.ts +15 -3
- package/src/tool/subcall-store.ts +57 -1
- package/src/ui/config-panel.ts +4 -16
- package/src/ui/intro.ts +1 -2
- package/src/ui/model-picker.ts +34 -10
- package/src/ui/status.ts +3 -7
- package/src/util/concurrency.ts +91 -13
- package/src/util/trace.ts +42 -0
- package/src/bridge/fallback-todo.ts +0 -137
- package/src/bridge/interactive.ts +0 -65
- package/src/bridge/llm-query.ts +0 -156
- package/src/bridge/pi-interactive.ts +0 -41
- package/src/bridge/rlm-query.ts +0 -108
- package/src/core/artifacts.ts +0 -89
- package/src/core/critique.ts +0 -92
- package/src/core/gates.ts +0 -301
- package/src/core/pipeline-handlers.ts +0 -319
- package/src/core/pipeline.ts +0 -268
- package/src/prompts/phases.ts +0 -104
- package/src/sandbox/worker.py +0 -1078
- package/src/state/index.ts +0 -24
- package/src/state/internal.ts +0 -46
- package/src/state/paths.ts +0 -44
- package/src/state/reads.ts +0 -133
- package/src/state/resume.ts +0 -173
- package/src/state/rows.ts +0 -123
- package/src/state/writes.ts +0 -58
package/src/config/defaults.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { RlmConfig } from "../core/types.ts";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
export const DEFAULT_RUN_DIR = join(tmpdir(), "rlm-runs");
|
|
6
2
|
|
|
7
3
|
/** Frozen default sub-LLM system prompt — avoids re-allocation on every llm_query call. */
|
|
8
4
|
const DEFAULT_SUB_SYSTEM_PROMPT =
|
|
@@ -16,26 +12,22 @@ export const DEFAULT_CONFIG: Readonly<RlmConfig> = Object.freeze({
|
|
|
16
12
|
maxIterations: 30,
|
|
17
13
|
execTimeoutS: 120,
|
|
18
14
|
requestTimeoutMs: 10 * 60_000,
|
|
19
|
-
|
|
15
|
+
// Session-wide, not per-batch: spawn() puts many requests on the wire at once, so this is
|
|
16
|
+
// the only thing bounding leaf fan-out.
|
|
17
|
+
maxConcurrentSubcalls: 16,
|
|
18
|
+
// Children are bounded separately and lower: each is a Python subprocess holding its own copy
|
|
19
|
+
// of the context it inherited, where a leaf is one HTTP request. Worst case is
|
|
20
|
+
// (maxDepth - 1) × this many concurrent child engines.
|
|
21
|
+
maxConcurrentChildren: 6,
|
|
20
22
|
maxPromptChars: 400_000,
|
|
21
23
|
maxErrors: 5,
|
|
22
24
|
orchestrator: true,
|
|
23
|
-
pipeline: false,
|
|
24
|
-
maxBackwardJumps: 2,
|
|
25
25
|
compaction: true,
|
|
26
26
|
compactionThresholdPct: 0.65,
|
|
27
27
|
python: "python3",
|
|
28
28
|
sandboxInitTimeoutMs: 30_000,
|
|
29
|
-
askUserQuestion: true,
|
|
30
|
-
todo: true,
|
|
31
29
|
libraryLoader: true,
|
|
32
30
|
rootSampling: Object.freeze({ maxTokens: 16_384 }),
|
|
33
31
|
subSystemPrompt: DEFAULT_SUB_SYSTEM_PROMPT,
|
|
34
32
|
subSampling: Object.freeze({ maxTokens: 8192 }),
|
|
35
|
-
runLog: Object.freeze({
|
|
36
|
-
enabled: true,
|
|
37
|
-
dir: DEFAULT_RUN_DIR,
|
|
38
|
-
snapshot: true,
|
|
39
|
-
maxRuns: 50,
|
|
40
|
-
}),
|
|
41
33
|
});
|
package/src/config/settings.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
/** Persist RLM settings (tunable config +
|
|
1
|
+
/** Persist RLM settings (tunable config + pinned sub-LLM model id). */
|
|
2
2
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { getAgentDir, type ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
7
|
-
import type { RlmConfig
|
|
7
|
+
import type { RlmConfig } from "../core/types.ts";
|
|
8
8
|
import { DEFAULT_CONFIG } from "./defaults.ts";
|
|
9
9
|
|
|
10
10
|
export interface PersistedSettings {
|
|
11
11
|
readonly config: Partial<RlmConfig>;
|
|
12
|
-
|
|
12
|
+
/** "provider/id" of the pinned sub-LLM, or undefined for "cheapest (auto)". */
|
|
13
|
+
readonly llm?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
type MutablePartialRlmConfig = { -readonly [K in keyof RlmConfig]?: RlmConfig[K] };
|
|
@@ -43,21 +44,6 @@ function validateThinkingLevel(v: unknown): ThinkingLevel | undefined {
|
|
|
43
44
|
return typeof v === "string" && Object.hasOwn(THINKING_LEVELS, v) ? (v as ThinkingLevel) : undefined;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function validateRunLog(raw: unknown): Partial<RunLogConfig> | undefined {
|
|
47
|
-
if (typeof raw !== "object" || raw === null) return undefined;
|
|
48
|
-
const r = raw as Record<string, unknown>;
|
|
49
|
-
const out: { enabled?: boolean; dir?: string; snapshot?: boolean; maxRuns?: number } = {};
|
|
50
|
-
const enabled = validateBoolean(r.enabled);
|
|
51
|
-
if (enabled !== undefined) out.enabled = enabled;
|
|
52
|
-
const dir = validateString(r.dir);
|
|
53
|
-
if (dir !== undefined) out.dir = dir;
|
|
54
|
-
const snapshot = validateBoolean(r.snapshot);
|
|
55
|
-
if (snapshot !== undefined) out.snapshot = snapshot;
|
|
56
|
-
const maxRuns = validateNumber(r.maxRuns, 1);
|
|
57
|
-
if (maxRuns !== undefined) out.maxRuns = maxRuns;
|
|
58
|
-
return Object.keys(out).length > 0 ? Object.freeze(out) : undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
47
|
function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
62
48
|
if (typeof raw !== "object" || raw === null) return {};
|
|
63
49
|
const r = raw as Record<string, unknown>;
|
|
@@ -74,10 +60,10 @@ function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
|
74
60
|
if (requestTimeoutMs !== undefined) out.requestTimeoutMs = requestTimeoutMs;
|
|
75
61
|
const maxConcurrentSubcalls = validateNumber(r.maxConcurrentSubcalls, 1);
|
|
76
62
|
if (maxConcurrentSubcalls !== undefined) out.maxConcurrentSubcalls = maxConcurrentSubcalls;
|
|
63
|
+
const maxConcurrentChildren = validateNumber(r.maxConcurrentChildren, 1);
|
|
64
|
+
if (maxConcurrentChildren !== undefined) out.maxConcurrentChildren = maxConcurrentChildren;
|
|
77
65
|
const maxPromptChars = validateNumber(r.maxPromptChars, 1000);
|
|
78
66
|
if (maxPromptChars !== undefined) out.maxPromptChars = maxPromptChars;
|
|
79
|
-
const maxBudgetUsd = validateNumber(r.maxBudgetUsd, 0.01);
|
|
80
|
-
if (maxBudgetUsd !== undefined) out.maxBudgetUsd = maxBudgetUsd;
|
|
81
67
|
const maxTimeoutMs = validateNumber(r.maxTimeoutMs, 1000);
|
|
82
68
|
if (maxTimeoutMs !== undefined) out.maxTimeoutMs = maxTimeoutMs;
|
|
83
69
|
const maxTokens = validateNumber(r.maxTokens, 1);
|
|
@@ -86,10 +72,6 @@ function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
|
86
72
|
if (maxErrors !== undefined) out.maxErrors = maxErrors;
|
|
87
73
|
const orchestrator = validateBoolean(r.orchestrator);
|
|
88
74
|
if (orchestrator !== undefined) out.orchestrator = orchestrator;
|
|
89
|
-
const pipeline = validateBoolean(r.pipeline);
|
|
90
|
-
if (pipeline !== undefined) out.pipeline = pipeline;
|
|
91
|
-
const maxBackwardJumps = validateNumber(r.maxBackwardJumps, 0);
|
|
92
|
-
if (maxBackwardJumps !== undefined) out.maxBackwardJumps = maxBackwardJumps;
|
|
93
75
|
const compaction = validateBoolean(r.compaction);
|
|
94
76
|
if (compaction !== undefined) out.compaction = compaction;
|
|
95
77
|
const compactionThresholdPct = validateNumber(r.compactionThresholdPct, 0);
|
|
@@ -100,14 +82,8 @@ function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
|
100
82
|
if (smartReasoning !== undefined) out.smartReasoning = smartReasoning;
|
|
101
83
|
const subSystemPrompt = validateString(r.subSystemPrompt);
|
|
102
84
|
if (subSystemPrompt !== undefined) out.subSystemPrompt = subSystemPrompt;
|
|
103
|
-
const runLog = validateRunLog(r.runLog);
|
|
104
|
-
if (runLog) out.runLog = runLog;
|
|
105
85
|
const sandboxInitTimeoutMs = validateNumber(r.sandboxInitTimeoutMs, 100);
|
|
106
86
|
if (sandboxInitTimeoutMs !== undefined) out.sandboxInitTimeoutMs = sandboxInitTimeoutMs;
|
|
107
|
-
const askUserQuestion = validateBoolean(r.askUserQuestion);
|
|
108
|
-
if (askUserQuestion !== undefined) out.askUserQuestion = askUserQuestion;
|
|
109
|
-
const todo = validateBoolean(r.todo);
|
|
110
|
-
if (todo !== undefined) out.todo = todo;
|
|
111
87
|
const libraryLoader = validateBoolean(r.libraryLoader);
|
|
112
88
|
if (libraryLoader !== undefined) out.libraryLoader = libraryLoader;
|
|
113
89
|
if (typeof r.subSampling === "object" && r.subSampling !== null) {
|
|
@@ -142,7 +118,8 @@ export async function loadSettings(): Promise<PersistedSettings> {
|
|
|
142
118
|
const r = raw as Record<string, unknown>;
|
|
143
119
|
return {
|
|
144
120
|
config: validateConfig(r.config),
|
|
145
|
-
worker
|
|
121
|
+
// `worker` is the pre-rename key — still read so an existing pin survives the upgrade.
|
|
122
|
+
llm: validateString(r.llm) ?? validateString(r.worker),
|
|
146
123
|
};
|
|
147
124
|
} catch {
|
|
148
125
|
return { config: {} };
|
|
@@ -167,7 +144,6 @@ export function mergeConfig(partial: Partial<RlmConfig>): RlmConfig {
|
|
|
167
144
|
...partial,
|
|
168
145
|
subSampling: { ...DEFAULT_CONFIG.subSampling, ...partial.subSampling },
|
|
169
146
|
rootSampling: Object.freeze({ ...DEFAULT_CONFIG.rootSampling, ...partial.rootSampling }),
|
|
170
|
-
...(partial.runLog ? { runLog: Object.freeze({ ...DEFAULT_CONFIG.runLog, ...partial.runLog }) } : {}),
|
|
171
147
|
};
|
|
172
148
|
}
|
|
173
149
|
|
|
@@ -31,7 +31,7 @@ const execFileP = promisify(execFile);
|
|
|
31
31
|
/** Single-file sources above this must use open() + llm_query_chunked in the REPL. */
|
|
32
32
|
export const MAX_LIBRARY_FILE_BYTES = 8 * 1024 * 1024;
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/** Catch-all prefix for a raw string payload with no namespace — never an identity key. */
|
|
35
35
|
const LEGACY_UNKNOWN_PREFIX = "lib/unknown/";
|
|
36
36
|
|
|
37
37
|
export interface LibrarySource {
|
|
@@ -150,23 +150,100 @@ export function namespaceLibraryFiles(
|
|
|
150
150
|
return namespaceLibraryFilesWithChars(payload, sourceId).files;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/** The one `lib/<id>/` matcher. Never re-declare this regex; use the helpers below. */
|
|
154
|
+
const LIB_PREFIX_RE = /^(lib\/[^/]+\/)/;
|
|
155
|
+
|
|
156
|
+
/** Narrow an unknown context entry to a ContextFile. Type guard, never a cast. */
|
|
157
|
+
export function isContextFile(entry: unknown): entry is ContextFile {
|
|
158
|
+
if (entry === null || typeof entry !== "object") return false;
|
|
159
|
+
return "path" in entry && typeof entry.path === "string"
|
|
160
|
+
&& "content" in entry && typeof entry.content === "string";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `path` of a context entry, or undefined when the entry is not file-shaped. */
|
|
164
|
+
export function contextEntryPath(entry: unknown): string | undefined {
|
|
165
|
+
return isContextFile(entry) ? entry.path : undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The `lib/<id>/` prefix owning this path, or undefined. Skips the legacy catch-all. */
|
|
169
|
+
function libPrefixOf(path: string): string | undefined {
|
|
170
|
+
const prefix = LIB_PREFIX_RE.exec(path)?.[1];
|
|
171
|
+
// `lib/unknown/` is the legacy catch-all: never treat it as an identity.
|
|
172
|
+
return prefix === undefined || prefix === LEGACY_UNKNOWN_PREFIX ? undefined : prefix;
|
|
173
|
+
}
|
|
174
|
+
|
|
153
175
|
/** First `lib/<id>/` prefix found in the payload, or undefined. Skips `lib/unknown/`. */
|
|
154
176
|
export function payloadPrefix(payload: readonly unknown[]): string | undefined {
|
|
155
177
|
for (let i = 0; i < payload.length; i++) {
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
const m = /^(lib\/[^/]+\/)/.exec(path);
|
|
161
|
-
// `lib/unknown/` is the legacy catch-all: never treat it as an identity.
|
|
162
|
-
if (m?.[1] !== undefined && m[1] !== LEGACY_UNKNOWN_PREFIX) return m[1];
|
|
178
|
+
const path = contextEntryPath(payload[i]);
|
|
179
|
+
if (path === undefined) continue;
|
|
180
|
+
const prefix = libPrefixOf(path);
|
|
181
|
+
if (prefix !== undefined) return prefix;
|
|
163
182
|
}
|
|
164
183
|
return undefined;
|
|
165
184
|
}
|
|
166
185
|
|
|
167
186
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
187
|
+
* Every distinct `lib/<id>/` prefix present in a context payload.
|
|
188
|
+
*
|
|
189
|
+
* The loaded-prefix set in bridge/library.ts is a CACHE of this — derived state, never
|
|
190
|
+
* independent state, so it may only be cleared by re-deriving it from the live payload.
|
|
191
|
+
*/
|
|
192
|
+
export function libraryPrefixesIn(context: unknown): readonly string[] {
|
|
193
|
+
if (!Array.isArray(context)) return Object.freeze([]);
|
|
194
|
+
const seen = new Set<string>();
|
|
195
|
+
for (let i = 0; i < context.length; i++) {
|
|
196
|
+
const path = contextEntryPath(context[i]);
|
|
197
|
+
if (path === undefined) continue;
|
|
198
|
+
const prefix = libPrefixOf(path);
|
|
199
|
+
if (prefix !== undefined) seen.add(prefix);
|
|
200
|
+
}
|
|
201
|
+
return Object.freeze(Array.from(seen));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface FilteredContext {
|
|
205
|
+
readonly files: readonly ContextFile[];
|
|
206
|
+
/** Prefixes that selected zero files — the caller decides whether that is fatal. */
|
|
207
|
+
readonly unmatched: readonly string[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Narrow a context payload to entries under any of `prefixes` (plain prefix match, no globs).
|
|
212
|
+
*
|
|
213
|
+
* Backs `rlm_query(prompt, paths=[…])`. Prefix-only is deliberate: the sandbox's own filters use
|
|
214
|
+
* Python `fnmatch`, which has no host-side equivalent here, and a subtree prefix is what callers
|
|
215
|
+
* actually want — the child can still `search()` inside the slice.
|
|
216
|
+
*/
|
|
217
|
+
export function filterContextByPaths(context: unknown, prefixes: readonly string[]): FilteredContext {
|
|
218
|
+
if (!Array.isArray(context) || prefixes.length === 0) {
|
|
219
|
+
return Object.freeze({ files: Object.freeze([]), unmatched: Object.freeze(Array.from(prefixes)) });
|
|
220
|
+
}
|
|
221
|
+
const hit = new Array<boolean>(prefixes.length).fill(false);
|
|
222
|
+
const out = new Array<ContextFile>(context.length); // pre-allocated, trimmed once below
|
|
223
|
+
let n = 0;
|
|
224
|
+
for (let i = 0; i < context.length; i++) {
|
|
225
|
+
const entry: unknown = context[i];
|
|
226
|
+
if (!isContextFile(entry)) continue;
|
|
227
|
+
for (let p = 0; p < prefixes.length; p++) {
|
|
228
|
+
if (!entry.path.startsWith(prefixes[p])) continue;
|
|
229
|
+
hit[p] = true;
|
|
230
|
+
out[n++] = entry;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
out.length = n;
|
|
235
|
+
const unmatched = new Array<string>(prefixes.length); // pre-allocated, no .push()
|
|
236
|
+
let u = 0;
|
|
237
|
+
for (let p = 0; p < prefixes.length; p++) {
|
|
238
|
+
if (!hit[p]) unmatched[u++] = prefixes[p];
|
|
239
|
+
}
|
|
240
|
+
unmatched.length = u;
|
|
241
|
+
return Object.freeze({ files: Object.freeze(out), unmatched: Object.freeze(unmatched) });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Append a library payload into an existing list context.
|
|
246
|
+
* Skips a payload whose path prefix is already present, so a repeat load is a no-op.
|
|
170
247
|
*/
|
|
171
248
|
export function mergeLibraryIntoContext(base: unknown, libraryPayload: unknown): unknown {
|
|
172
249
|
if (!Array.isArray(base)) return base;
|
|
@@ -175,12 +252,8 @@ export function mergeLibraryIntoContext(base: unknown, libraryPayload: unknown):
|
|
|
175
252
|
const prefix = payloadPrefix(libraryPayload);
|
|
176
253
|
if (prefix !== undefined) {
|
|
177
254
|
for (let i = 0; i < base.length; i++) {
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
180
|
-
&& typeof (item as { path?: unknown }).path === "string"
|
|
181
|
-
&& (item as { path: string }).path.startsWith(prefix)) {
|
|
182
|
-
return base; // already present
|
|
183
|
-
}
|
|
255
|
+
const path = contextEntryPath(base[i]);
|
|
256
|
+
if (path !== undefined && path.startsWith(prefix)) return base; // already present
|
|
184
257
|
}
|
|
185
258
|
}
|
|
186
259
|
const merged = new Array<unknown>(base.length + libraryPayload.length);
|
|
@@ -189,7 +262,7 @@ export function mergeLibraryIntoContext(base: unknown, libraryPayload: unknown):
|
|
|
189
262
|
return merged;
|
|
190
263
|
}
|
|
191
264
|
if (typeof libraryPayload === "string") {
|
|
192
|
-
//
|
|
265
|
+
// Raw string payload: wrap once under the unknown prefix.
|
|
193
266
|
return mergeLibraryIntoContext(base, namespaceLibraryFiles(libraryPayload, "unknown"));
|
|
194
267
|
}
|
|
195
268
|
return base;
|