@hicaru/pi-rlm 0.1.8 → 0.2.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/README.md +22 -19
- package/package.json +2 -1
- package/src/bridge/library.ts +93 -15
- package/src/bridge/llm-query.ts +60 -36
- package/src/bridge/rlm-query.ts +63 -79
- package/src/commands/rlm-config.ts +8 -8
- package/src/commands/rlm.ts +48 -12
- package/src/config/settings.ts +33 -3
- package/src/context/library-context.ts +209 -22
- package/src/context/repomix-context.ts +7 -58
- package/src/core/answer.ts +5 -13
- package/src/core/artifacts.ts +4 -3
- package/src/core/critique.ts +92 -0
- package/src/core/engine.ts +94 -299
- package/src/core/gates.ts +33 -4
- package/src/core/limits.ts +19 -1
- package/src/core/pipeline-handlers.ts +319 -0
- package/src/core/pipeline.ts +40 -15
- package/src/core/types.ts +26 -30
- package/src/index.ts +36 -26
- package/src/mode/native-guards.ts +2 -2
- package/src/mode/rlm-mode.ts +8 -11
- package/src/prompts/phases.ts +18 -39
- package/src/prompts/system.ts +167 -64
- package/src/prompts/user.ts +1 -5
- package/src/sandbox/protocol.ts +5 -17
- package/src/sandbox/sandbox-manager.ts +5 -5
- package/src/sandbox/sandbox.ts +67 -27
- package/src/sandbox/worker.py +534 -48
- package/src/state/paths.ts +1 -1
- package/src/state/reads.ts +12 -4
- package/src/state/resume.ts +26 -25
- package/src/state/rows.ts +2 -2
- package/src/text/parsing.ts +0 -6
- package/src/text/tokens.ts +7 -1
- package/src/tool/repl-details.ts +2 -3
- package/src/tool/repl-tool.ts +132 -337
- package/src/tool/rlm-aggregator.ts +7 -7
- package/src/tool/rlm-details.ts +6 -13
- package/src/tool/rlm-events.ts +14 -11
- package/src/tool/rlm-tool.ts +20 -38
- package/src/tool/subcall-render.ts +61 -9
- package/src/tool/subcall-store.ts +4 -2
- package/src/ui/config-panel.ts +43 -23
- 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/mode/input-router.ts +0 -23
- package/src/registry/edit-registry.ts +0 -22
- package/src/text/edits.ts +0 -164
- package/src/tool/apply-edits-tool.ts +0 -295
package/src/index.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/** pi-rlm — Recursive Language Model for Pi. */
|
|
2
2
|
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import { Markdown } from "@earendil-works/pi-tui";
|
|
6
5
|
import { registerRlmCommand } from "./commands/rlm.ts";
|
|
7
6
|
import { registerRlmConfigCommand } from "./commands/rlm-config.ts";
|
|
8
7
|
import { createRlmTool } from "./tool/rlm-tool.ts";
|
|
9
8
|
import { createReplTool } from "./tool/repl-tool.ts";
|
|
10
|
-
import { createApplyEditsTool } from "./tool/apply-edits-tool.ts";
|
|
11
|
-
import { EditRegistry } from "./registry/edit-registry.ts";
|
|
12
9
|
import { loadSettings, mergeConfig, resolveModelId } from "./config/settings.ts";
|
|
13
10
|
import { RlmController, cheapestModel } from "./mode/rlm-mode.ts";
|
|
14
11
|
import { postRlmGuide } from "./ui/intro.ts";
|
|
15
12
|
import { setRlmModeStatus } from "./ui/status.ts";
|
|
13
|
+
import { markdownTheme } from "./ui/theme-adapter.ts";
|
|
16
14
|
import { SandboxManager } from "./sandbox/sandbox-manager.ts";
|
|
17
15
|
import { packRepository, formatForLLM, serializeForSandbox } from "./context/repomix-context.ts";
|
|
18
16
|
import { buildNativeSystemPrompt, NATIVE_TURN_REMINDER } from "./prompts/system.ts";
|
|
@@ -26,7 +24,6 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
26
24
|
// Init synchronously with defaults — ensures commands/tools/handlers register before session_start
|
|
27
25
|
const config = mergeConfig({});
|
|
28
26
|
const controller = new RlmController(config);
|
|
29
|
-
const editRegistry = new EditRegistry();
|
|
30
27
|
let onSandboxDiscardExtra: (() => void) | undefined;
|
|
31
28
|
const sandboxManager = new SandboxManager({
|
|
32
29
|
execTimeoutS: config.execTimeoutS,
|
|
@@ -34,10 +31,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
34
31
|
python: config.python,
|
|
35
32
|
sandboxInitTimeoutMs: config.sandboxInitTimeoutMs,
|
|
36
33
|
maxPromptChars: config.maxPromptChars,
|
|
37
|
-
onSandboxDiscarded: () => {
|
|
38
|
-
editRegistry.clear();
|
|
39
|
-
onSandboxDiscardExtra?.();
|
|
40
|
-
},
|
|
34
|
+
onSandboxDiscarded: () => { onSandboxDiscardExtra?.(); },
|
|
41
35
|
});
|
|
42
36
|
let packedContextText: string | undefined;
|
|
43
37
|
let contextPackPromise: Promise<string | undefined> | undefined;
|
|
@@ -68,30 +62,40 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
68
62
|
});
|
|
69
63
|
|
|
70
64
|
// ── Message renderers ──
|
|
71
|
-
pi
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
// Markdown themes are derived from the injected `theme`, never pi's module-global
|
|
66
|
+
// `getMarkdownTheme()` — under jiti that global can be undefined inside a plugin.
|
|
67
|
+
pi.registerMessageRenderer("rlm-answer", (message, _options, theme) =>
|
|
68
|
+
new Markdown(String(message.content ?? ""), 1, 0, markdownTheme(theme)),
|
|
74
69
|
);
|
|
75
|
-
pi.registerMessageRenderer("rlm-question", (message, _options,
|
|
76
|
-
new Markdown(`**RLM question**\n\n${String(message.content ?? "")}`, 1, 0,
|
|
70
|
+
pi.registerMessageRenderer("rlm-question", (message, _options, theme) =>
|
|
71
|
+
new Markdown(`**RLM question**\n\n${String(message.content ?? "")}`, 1, 0, markdownTheme(theme)),
|
|
77
72
|
);
|
|
78
|
-
pi.registerMessageRenderer("rlm-intro", (message, _options,
|
|
79
|
-
new Markdown(String(message.content ?? ""), 1, 0,
|
|
73
|
+
pi.registerMessageRenderer("rlm-intro", (message, _options, theme) =>
|
|
74
|
+
new Markdown(String(message.content ?? ""), 1, 0, markdownTheme(theme)),
|
|
80
75
|
);
|
|
81
76
|
|
|
77
|
+
// ── CLI flag: `pi --rlm` / `pi --rlm=false` overrides the persisted mode for this run ──
|
|
78
|
+
pi.registerFlag("rlm", {
|
|
79
|
+
description: "Start with RLM mode on (repl-only repository reading).",
|
|
80
|
+
type: "boolean",
|
|
81
|
+
});
|
|
82
|
+
|
|
82
83
|
// ── Commands ──
|
|
83
84
|
registerRlmCommand(pi, controller);
|
|
84
85
|
registerRlmConfigCommand(pi, controller);
|
|
85
86
|
|
|
86
87
|
// ── Tool registration ──
|
|
87
88
|
pi.registerTool(createRlmTool(controller));
|
|
88
|
-
pi.registerTool(createApplyEditsTool(editRegistry));
|
|
89
89
|
let guidePosted = false;
|
|
90
90
|
|
|
91
91
|
pi.on("session_start", async (_event, ctx) => {
|
|
92
92
|
// Wait for persisted settings before reading controller state
|
|
93
93
|
await settingsReady;
|
|
94
94
|
|
|
95
|
+
// An explicit --rlm flag wins over the persisted setting for this session.
|
|
96
|
+
const flag = pi.getFlag("rlm");
|
|
97
|
+
if (typeof flag === "boolean") controller.setConfig(Object.freeze({ ...controller.config, enabled: flag }));
|
|
98
|
+
|
|
95
99
|
if (controller.savedWorkerRef) {
|
|
96
100
|
const resolved = resolveModelId(ctx.modelRegistry, controller.savedWorkerRef);
|
|
97
101
|
if (resolved) controller.workerModel = resolved;
|
|
@@ -109,24 +113,34 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
109
113
|
getModel: () => controller.resolveModels(ctx)?.model,
|
|
110
114
|
getWorkerModel: () => controller.resolveModels(ctx)?.worker,
|
|
111
115
|
registry: ctx.modelRegistry,
|
|
112
|
-
|
|
113
|
-
config: controller.config,
|
|
116
|
+
getConfig: () => controller.config,
|
|
114
117
|
registerDiscardHook: (reset) => { onSandboxDiscardExtra = reset; },
|
|
115
118
|
ensureContext: async () => {
|
|
116
119
|
const contextText = await ensureRepositoryContext(ctx.cwd ?? process.cwd());
|
|
117
120
|
if (contextText === undefined) throw new Error("repository context could not be loaded into RLM sandbox");
|
|
118
121
|
},
|
|
119
122
|
}));
|
|
120
|
-
} catch {
|
|
123
|
+
} catch (err) {
|
|
124
|
+
// Re-registering the same tool each session is expected; anything else is a real failure.
|
|
125
|
+
const message = errorMessage(err);
|
|
126
|
+
if (!/already (registered|exists)/i.test(message)) {
|
|
127
|
+
console.warn(`[rlm] repl tool registration failed: ${message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
setRlmModeStatus(ctx.ui, controller);
|
|
132
|
+
setRlmModeStatus(ctx.ui, controller, ctx.getContextUsage());
|
|
124
133
|
if (!guidePosted && controller.enabled) {
|
|
125
134
|
guidePosted = true;
|
|
126
135
|
postRlmGuide(pi, controller);
|
|
127
136
|
}
|
|
128
137
|
});
|
|
129
138
|
|
|
139
|
+
// ── Keep the footer's context reading live (RLM exists to shrink this number) ──
|
|
140
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
141
|
+
setRlmModeStatus(ctx.ui, controller, ctx.getContextUsage());
|
|
142
|
+
});
|
|
143
|
+
|
|
130
144
|
// ── System prompt: native RLM mode addendum (only when enabled) ──
|
|
131
145
|
pi.on("before_agent_start", async (event) => {
|
|
132
146
|
if (!controller.enabled) return;
|
|
@@ -154,7 +168,8 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
154
168
|
const instruction = [
|
|
155
169
|
"ANALYZE THIS REPOSITORY using repl({code}) — read/grep are DISABLED.",
|
|
156
170
|
"Repository contents are pre-loaded in the Python REPL `context` variable.",
|
|
157
|
-
"
|
|
171
|
+
"Locate with search()/grep_context()/outline() (free), then delegate bulk reading to",
|
|
172
|
+
"map_files()/llm_query_batched(). If credits exhausted → report and stop.",
|
|
158
173
|
"",
|
|
159
174
|
].join("\n");
|
|
160
175
|
filtered.unshift({
|
|
@@ -175,11 +190,6 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
175
190
|
return { messages: filtered };
|
|
176
191
|
});
|
|
177
192
|
|
|
178
|
-
// ── Input routing: native mode — agent decides whether to use repl() or other tools ──
|
|
179
|
-
pi.on("input", async (_event, _ctx) => {
|
|
180
|
-
return { action: "continue" };
|
|
181
|
-
});
|
|
182
|
-
|
|
183
193
|
// ── Native mode restrictions: keep bulk file content out of root-model context ──
|
|
184
194
|
// `edit`/`write` stay unblocked so the agent modifies files through Pi's native
|
|
185
195
|
// tool flow (visible to all plugins, +/- diff preview). File reading/searching
|
|
@@ -86,7 +86,7 @@ export function replDelegationNudge(stdoutChars: number, delegated: boolean): st
|
|
|
86
86
|
if (delegated || stdoutChars <= NUDGE_STDOUT_CHARS) return undefined;
|
|
87
87
|
return (
|
|
88
88
|
`\n[RLM: this repl() printed ${stdoutChars.toLocaleString()} chars with 0 sub-LLM calls — ` +
|
|
89
|
-
"
|
|
90
|
-
"
|
|
89
|
+
"if you were READING, delegate via llm_query / llm_query_batched / llm_query_chunked. " +
|
|
90
|
+
"Authoring an edit body yourself is correct and needs no delegation.]"
|
|
91
91
|
);
|
|
92
92
|
}
|
package/src/mode/rlm-mode.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { ExtensionContext, ModelRegistry } from "@earendil-works/pi-coding-
|
|
|
11
11
|
import { DEFAULT_RUN_DIR } from "../config/defaults.ts";
|
|
12
12
|
import { modelRef, resolveModelId, saveSettings } from "../config/settings.ts";
|
|
13
13
|
import { createEngine } from "../core/engine.ts";
|
|
14
|
+
import { limitsFromConfig } from "../core/limits.ts";
|
|
14
15
|
import type { InteractiveDeps, RlmConfig, RlmInput, RlmResult } from "../core/types.ts";
|
|
15
16
|
import type { ReconstructResult } from "../state/resume.ts";
|
|
16
17
|
import { packRepository, serializeForSandbox } from "../context/repomix-context.ts";
|
|
@@ -44,8 +45,13 @@ export class RlmController {
|
|
|
44
45
|
return this.config.enabled;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/** Replace the config wholesale — `RlmConfig` is immutable, so edits produce a new object. */
|
|
49
|
+
setConfig(config: RlmConfig): void {
|
|
50
|
+
this.config = config;
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
setEnabled(enabled: boolean): void {
|
|
48
|
-
this.config
|
|
54
|
+
this.config = Object.freeze({ ...this.config, enabled });
|
|
49
55
|
void this.persist();
|
|
50
56
|
}
|
|
51
57
|
|
|
@@ -56,10 +62,6 @@ export class RlmController {
|
|
|
56
62
|
return next;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
hasSavedModels(): boolean {
|
|
60
|
-
return Boolean(this.savedWorkerRef || this.workerModel);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
65
|
async persist(): Promise<boolean> {
|
|
64
66
|
return await saveSettings({
|
|
65
67
|
config: this.config,
|
|
@@ -132,12 +134,7 @@ export class RlmController {
|
|
|
132
134
|
runState,
|
|
133
135
|
onAskUserQuestion: interactive?.onAskUserQuestion,
|
|
134
136
|
onTodo: interactive?.onTodo,
|
|
135
|
-
limits:
|
|
136
|
-
maxBudgetUsd: this.config.maxBudgetUsd,
|
|
137
|
-
maxTimeoutMs: this.config.maxTimeoutMs,
|
|
138
|
-
maxTokens: this.config.maxTokens,
|
|
139
|
-
maxErrors: this.config.maxErrors,
|
|
140
|
-
},
|
|
137
|
+
limits: limitsFromConfig(this.config),
|
|
141
138
|
});
|
|
142
139
|
return await engine(engineInput);
|
|
143
140
|
})().finally(() => {
|
package/src/prompts/phases.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Injected at each phase boundary; the only channel between phases is durable
|
|
4
4
|
* artifacts under .rlm/artifacts/.
|
|
5
5
|
*/
|
|
6
|
-
import type { PhaseRecord } from "../core/gates.ts";
|
|
7
6
|
import type { Phase } from "../core/pipeline.ts";
|
|
8
7
|
|
|
9
8
|
const CLARIFY_GUIDANCE = Object.freeze([
|
|
@@ -48,7 +47,7 @@ const RESEARCH_GUIDANCE = Object.freeze([
|
|
|
48
47
|
"Probe the repository, delegate long reads via `llm_query_batched` / `llm_query_chunked`.",
|
|
49
48
|
"Every factual claim about the code MUST be cited as `path/file.ext:LINE` (or `LINE-LINE`).",
|
|
50
49
|
"The engine VERIFIES citations against the working tree — unbacked citations reject advance.",
|
|
51
|
-
"When research is complete, write ONE research document and call:",
|
|
50
|
+
"When research is complete, write ONE research document with a `## Findings` section and call:",
|
|
52
51
|
' `save_artifact("research", content)` # frontmatter must include `status: ready`',
|
|
53
52
|
' `advance_phase("blueprint", summary)`',
|
|
54
53
|
"Do not implement code in this phase.",
|
|
@@ -59,39 +58,37 @@ const BLUEPRINT_GUIDANCE = Object.freeze([
|
|
|
59
58
|
"Read the clarifications artifact (if present) and plan only within recorded Decisions;",
|
|
60
59
|
"Open Questions that would block the design must be re-asked via ask_user_question, not assumed.",
|
|
61
60
|
'Produce ONE plan document and save it with `save_artifact("plan", content)`.',
|
|
61
|
+
"This pipeline is read-only: it produces a plan a human or a native-mode agent executes;",
|
|
62
|
+
"do not write source files here (sandbox write modes are blocked).",
|
|
63
|
+
"Write changes as exact, copy-pasteable before/after code — never as prose instructions.",
|
|
62
64
|
"The ENGINE derive-checks the document — these are hard gates, not suggestions:",
|
|
63
65
|
"- frontmatter: `status: ready`, `phase_count: N`, `phases: [{n: 1, title: ...}, ...]`",
|
|
64
66
|
"- `phases:` / `phase_count` MUST match the `## Phase N:` body headings exactly (fenced examples ignored)",
|
|
65
67
|
"- every `file:line` citation MUST resolve against the working tree at this revision",
|
|
66
68
|
"Each `## Phase N: <title>` section contains:",
|
|
67
|
-
"- `### Changes Required` — per file: path + exact intended change
|
|
69
|
+
"- `### Changes Required` — per file: path + the exact intended change",
|
|
68
70
|
"- `### Success Criteria` — `#### Automated Verification:` (runnable commands) and",
|
|
69
71
|
" `#### Manual Verification:` checklists",
|
|
70
|
-
"Phases
|
|
71
|
-
|
|
72
|
-
"later phase unless that phase only EDITS what an earlier phase CREATED.",
|
|
73
|
-
'When ready: `advance_phase("implement", summary)` — the engine runs implement fanout itself.',
|
|
74
|
-
].join("\n"));
|
|
75
|
-
|
|
76
|
-
const IMPLEMENT_GUIDANCE = Object.freeze([
|
|
77
|
-
"## Implement phase",
|
|
78
|
-
"The engine drives serial child-RLM workers over each plan phase — you do not implement",
|
|
79
|
-
"here. If you were re-entered into implement unexpectedly, call",
|
|
80
|
-
'`advance_phase("validate")` only after the fanout has already completed.',
|
|
72
|
+
"Phases must be independently implementable in order, each leaving the tree working.",
|
|
73
|
+
'When ready: `advance_phase("validate", summary)`.',
|
|
81
74
|
].join("\n"));
|
|
82
75
|
|
|
83
76
|
const VALIDATE_GUIDANCE = Object.freeze([
|
|
84
|
-
"## Validate phase",
|
|
77
|
+
"## Validate phase (adversarial plan review)",
|
|
78
|
+
"Nothing has been implemented — you are reviewing the PLAN, not a diff.",
|
|
85
79
|
"Read the goal artifact (verbatim brief), the clarifications artifact (recorded Decisions),",
|
|
86
|
-
"and the plan.
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
80
|
+
"and the plan. For each plan phase check against the working tree:",
|
|
81
|
+
"- every cited `file:line` still resolves and says what the plan claims",
|
|
82
|
+
"- every file the phase edits exists (or is created by an earlier phase)",
|
|
83
|
+
"- no phase contradicts a recorded Decision or silently resolves an Open Question",
|
|
84
|
+
"- the Success Criteria are actually runnable and actually prove the change",
|
|
85
|
+
"- phases are independently implementable in the stated order",
|
|
86
|
+
"Write ONE review via `save_artifact(\"validation\", content)` with frontmatter:",
|
|
90
87
|
"- `status: ready`",
|
|
91
|
-
"- `blockers_count: <int
|
|
88
|
+
"- `blockers_count: <int >= 0>` # MEASURED — routing key; not prose",
|
|
92
89
|
"- `verdict: pass | fail` # pass requires blockers_count === 0",
|
|
93
90
|
"Each blocker needs a resolvable `file:line` citation.",
|
|
94
|
-
"Then finalize: `answer[\"content\"] = <
|
|
91
|
+
"Then finalize: `answer[\"content\"] = <the reviewed plan + review summary>; answer[\"ready\"] = True`.",
|
|
95
92
|
"If `blockers_count > 0`, the engine re-enters blueprint (bounded by maxBackwardJumps).",
|
|
96
93
|
].join("\n"));
|
|
97
94
|
|
|
@@ -99,27 +96,9 @@ const GUIDANCE: Readonly<Record<Phase, string>> = Object.freeze({
|
|
|
99
96
|
clarify: CLARIFY_GUIDANCE,
|
|
100
97
|
research: RESEARCH_GUIDANCE,
|
|
101
98
|
blueprint: BLUEPRINT_GUIDANCE,
|
|
102
|
-
implement: IMPLEMENT_GUIDANCE,
|
|
103
99
|
validate: VALIDATE_GUIDANCE,
|
|
104
100
|
});
|
|
105
101
|
|
|
106
102
|
export function phaseGuidance(phase: Phase): string {
|
|
107
103
|
return GUIDANCE[phase];
|
|
108
104
|
}
|
|
109
|
-
|
|
110
|
-
/** Single-phase implement prompt for an isolated child RLM (serial fanout unit). */
|
|
111
|
-
export function buildImplementPhasePrompt(planPath: string, r: PhaseRecord): string {
|
|
112
|
-
return [
|
|
113
|
-
`Implement ONLY Phase ${r.n} (${r.title}) of the plan at ${planPath} (${r.index + 1}/${r.total}).`,
|
|
114
|
-
`Read the plan from the REPL (open("${planPath}").read()).`,
|
|
115
|
-
"Hard rules (you are one unit of a sequenced run):",
|
|
116
|
-
"- CRITICAL: `context` is a STALE snapshot from run start. Always `open(path).read()` any file",
|
|
117
|
-
" you will edit BEFORE computing stage_edit anchors — earlier phases may have already changed them.",
|
|
118
|
-
"- Touch ONLY the files this phase's ### Changes Required lists.",
|
|
119
|
-
"- Earlier phases have already landed; a missing prerequisite file is a HARD ERROR —",
|
|
120
|
-
" finalize with 'Error: prerequisite missing: <path>' instead of creating it.",
|
|
121
|
-
"- Stage every change via stage_edit(path, old_text, new_text); never defer your own edits.",
|
|
122
|
-
"- Run only THIS phase's '#### Automated Verification:' commands; whole-plan checks are validate's job.",
|
|
123
|
-
`Finalize with a short summary of what landed for Phase ${r.n}.`,
|
|
124
|
-
].join("\n");
|
|
125
|
-
}
|