@hicaru/pi-rlm 0.1.0 → 0.1.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 +58 -131
- package/README.ru.md +6 -8
- package/README.zh-CN.md +6 -12
- package/package.json +10 -12
- package/src/bridge/pi-interactive.ts +3 -48
- package/src/commands/rlm-config.ts +10 -3
- package/src/commands/rlm.ts +0 -15
- package/src/config/settings.ts +1 -24
- package/src/core/answer.ts +1 -17
- package/src/core/engine.ts +9 -14
- package/src/core/types.ts +1 -13
- package/src/index.ts +52 -35
- package/src/prompts/system.ts +19 -37
- package/src/sandbox/protocol.ts +0 -6
- package/src/sandbox/sandbox-manager.ts +7 -5
- package/src/sandbox/sandbox.ts +4 -1
- package/src/sandbox/worker.py +12 -11
- package/src/tool/repl-details.ts +3 -0
- package/src/tool/repl-tool.ts +34 -5
- package/src/tool/rlm-events.ts +2 -41
- package/src/tool/rlm-tool.ts +0 -13
- package/src/ui/model-picker.ts +12 -4
- package/src/patch/apply.ts +0 -148
- package/src/patch/index.ts +0 -37
- package/src/state/events.ts +0 -22
- package/src/telemetry/dispatcher.ts +0 -116
- package/src/telemetry/index.ts +0 -14
- package/src/telemetry/mlflow-config.ts +0 -15
- package/src/telemetry/mlflow-sink.ts +0 -136
- package/src/telemetry/mlflow.ts +0 -99
- package/src/telemetry/sink.ts +0 -8
- package/src/tool/apply-diff-tool.ts +0 -125
package/src/core/engine.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { PythonSandbox } from "../sandbox/sandbox.ts";
|
|
|
21
21
|
import { advancePhase as validatePhaseTransition, phaseGatePrompt, type PhaseState } from "./pipeline.ts";
|
|
22
22
|
import { previewStdout, previewText } from "../text/preview.ts";
|
|
23
23
|
import { contextLength, contextSizeStats, contextTypeLabel } from "../text/tokens.ts";
|
|
24
|
-
import {
|
|
24
|
+
import { collectEdits, finalAnswerOf, formatReplOutputs, latestAnswerContentOf, turnHadError } from "./answer.ts";
|
|
25
25
|
import { compactHistory, shouldCompact } from "./compaction.ts";
|
|
26
26
|
import { appendUserMessage } from "./history.ts";
|
|
27
27
|
import { runTurn } from "./iteration.ts";
|
|
@@ -32,7 +32,7 @@ import { appendRow, appendTodoRow, generateRunId, pruneRuns, snapshotPath, write
|
|
|
32
32
|
import { STATE_SCHEMA_VERSION } from "../state/rows.ts";
|
|
33
33
|
import type { PhaseRow, RunHeader } from "../state/rows.ts";
|
|
34
34
|
import { serializeForSandbox, type ContextBundle } from "../context/repomix-context.ts";
|
|
35
|
-
import type {
|
|
35
|
+
import type { ProposedEdit } from "../sandbox/protocol.ts";
|
|
36
36
|
import { formatError } from "../util/errors.ts";
|
|
37
37
|
|
|
38
38
|
|
|
@@ -57,7 +57,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
57
57
|
const run: RunRlm = async (input: RlmInput): Promise<RlmResult> => {
|
|
58
58
|
const nowIso = (): string => new Date().toISOString(); // local helper — 4 call sites below
|
|
59
59
|
const persist = input.depth === 0 && deps.runState !== undefined;
|
|
60
|
-
// Compute runId early
|
|
60
|
+
// Compute runId early for run-state correlation on resume.
|
|
61
61
|
const runId = persist
|
|
62
62
|
? (input.resume ? input.resume.header.runId : generateRunId())
|
|
63
63
|
: undefined;
|
|
@@ -136,7 +136,6 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
136
136
|
let compactions = 0;
|
|
137
137
|
let completedTurns = 0;
|
|
138
138
|
let editsAcc: ProposedEdit[] = [];
|
|
139
|
-
let diffsAcc: ProposedDiffEdit[] = [];
|
|
140
139
|
let phaseState: PhaseState | undefined;
|
|
141
140
|
let nodeStatus: "done" | "error" = "done";
|
|
142
141
|
let persistOn = persist;
|
|
@@ -228,7 +227,6 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
228
227
|
limits.addRaw(input.resume.usageSeed.costUsd, input.resume.usageSeed.inputTokens, input.resume.usageSeed.outputTokens);
|
|
229
228
|
best = input.resume.best;
|
|
230
229
|
editsAcc = [];
|
|
231
|
-
diffsAcc = [];
|
|
232
230
|
compactions = input.resume.compactions;
|
|
233
231
|
completedTurns = input.resume.completedTurns;
|
|
234
232
|
if (input.resume.phase) {
|
|
@@ -312,12 +310,9 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
312
310
|
completedTurns = i + 1;
|
|
313
311
|
const proposedEdits = collectEdits(turn.results);
|
|
314
312
|
if (proposedEdits.length > 0) editsAcc = proposedEdits;
|
|
315
|
-
const proposedDiffs = collectDiffs(turn.results);
|
|
316
|
-
if (proposedDiffs.length > 0) diffsAcc = proposedDiffs;
|
|
317
|
-
|
|
318
313
|
const final = finalAnswerOf(turn.results);
|
|
319
314
|
if (final != null) {
|
|
320
|
-
const done = result(final, i + 1, limits, editsAcc
|
|
315
|
+
const done = result(final, i + 1, limits, editsAcc);
|
|
321
316
|
await recordTerminal("completed", done);
|
|
322
317
|
lastAnswer = done.answer;
|
|
323
318
|
return done;
|
|
@@ -348,21 +343,21 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
348
343
|
}
|
|
349
344
|
}
|
|
350
345
|
if (pendingReplOutputs) appendUserMessage(history, pendingReplOutputs);
|
|
351
|
-
const finalized = result(await finalize(history, deps, limits), deps.config.maxIterations, limits, editsAcc
|
|
346
|
+
const finalized = result(await finalize(history, deps, limits), deps.config.maxIterations, limits, editsAcc);
|
|
352
347
|
await recordTerminal("finalized", finalized);
|
|
353
348
|
lastAnswer = finalized.answer;
|
|
354
349
|
return finalized;
|
|
355
350
|
} catch (err) {
|
|
356
351
|
// Abort is a user action — resolve with the best partial, not an error.
|
|
357
352
|
if (deps.signal?.aborted) {
|
|
358
|
-
const aborted = result(best.trim() || "(aborted)", completedTurns, limits, editsAcc
|
|
353
|
+
const aborted = result(best.trim() || "(aborted)", completedTurns, limits, editsAcc);
|
|
359
354
|
await recordTerminal("aborted", aborted);
|
|
360
355
|
lastAnswer = aborted.answer;
|
|
361
356
|
return aborted;
|
|
362
357
|
}
|
|
363
358
|
if (err instanceof LimitError) {
|
|
364
359
|
nodeStatus = "error";
|
|
365
|
-
const stopped = result(best.trim() || `(stopped: ${err.message})`, completedTurns, limits, editsAcc
|
|
360
|
+
const stopped = result(best.trim() || `(stopped: ${err.message})`, completedTurns, limits, editsAcc);
|
|
366
361
|
await recordTerminal("stopped", stopped);
|
|
367
362
|
lastAnswer = stopped.answer;
|
|
368
363
|
return stopped;
|
|
@@ -388,9 +383,9 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
388
383
|
return run;
|
|
389
384
|
}
|
|
390
385
|
|
|
391
|
-
function result(answer: string, iterations: number, limits: LimitGuard, edits: ProposedEdit[] = []
|
|
386
|
+
function result(answer: string, iterations: number, limits: LimitGuard, edits: ProposedEdit[] = []): RlmResult {
|
|
392
387
|
const u = limits.usage();
|
|
393
|
-
return { answer, edits,
|
|
388
|
+
return { answer, edits, iterations, costUsd: u.costUsd, inputTokens: u.inputTokens, outputTokens: u.outputTokens, durationMs: u.durationMs };
|
|
394
389
|
}
|
|
395
390
|
|
|
396
391
|
/** Out of turns: ask the model for its best final answer (plain text). */
|
package/src/core/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Shared configuration + runtime types for the RLM engine. */
|
|
2
2
|
|
|
3
3
|
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { AskAnswer, AskQuestion,
|
|
4
|
+
import type { AskAnswer, AskQuestion, ProposedEdit } from "../sandbox/protocol.ts";
|
|
5
5
|
import type { ReconstructResult } from "../state/resume.ts";
|
|
6
6
|
|
|
7
7
|
export interface Sampling {
|
|
@@ -12,15 +12,6 @@ export interface Sampling {
|
|
|
12
12
|
|
|
13
13
|
type MutableSampling = { -readonly [Key in keyof Sampling]?: Sampling[Key] };
|
|
14
14
|
|
|
15
|
-
export interface TelemetryConfig {
|
|
16
|
-
/** Default: enabled iff a tracking URI resolves from config or MLFLOW_TRACKING_URI. */
|
|
17
|
-
readonly enabled?: boolean;
|
|
18
|
-
readonly trackingUri?: string;
|
|
19
|
-
readonly experimentId?: string;
|
|
20
|
-
/** Bearer token is env-only via MLFLOW_TRACKING_TOKEN; never persisted in rlm.json. */
|
|
21
|
-
readonly maxQueueSize?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
15
|
export interface RunLogConfig {
|
|
25
16
|
/** Default: true — always-on, opt-out. */
|
|
26
17
|
readonly enabled?: boolean;
|
|
@@ -81,8 +72,6 @@ export interface RlmConfig {
|
|
|
81
72
|
subSystemPrompt?: string;
|
|
82
73
|
/** Sampling for sub-LLM (worker) calls. */
|
|
83
74
|
subSampling: MutableSampling;
|
|
84
|
-
/** Optional MLflow telemetry export configuration. Omitted by default. */
|
|
85
|
-
readonly telemetry?: TelemetryConfig;
|
|
86
75
|
/** Optional run-state persistence configuration. Enabled by default. */
|
|
87
76
|
readonly runLog?: RunLogConfig;
|
|
88
77
|
}
|
|
@@ -112,7 +101,6 @@ export interface RlmResult {
|
|
|
112
101
|
readonly answer: string;
|
|
113
102
|
/** Legacy anchor edits retained for compatibility while older run-state rows exist. */
|
|
114
103
|
readonly edits?: readonly ProposedEdit[];
|
|
115
|
-
readonly diffs?: readonly ProposedDiffEdit[];
|
|
116
104
|
readonly iterations: number;
|
|
117
105
|
readonly costUsd: number;
|
|
118
106
|
readonly inputTokens: number;
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { Markdown } from "@earendil-works/pi-tui";
|
|
|
6
6
|
import { registerRlmCommand } from "./commands/rlm.ts";
|
|
7
7
|
import { registerRlmConfigCommand } from "./commands/rlm-config.ts";
|
|
8
8
|
import { createRlmTool } from "./tool/rlm-tool.ts";
|
|
9
|
-
import { createApplyDiffTool } from "./tool/apply-diff-tool.ts";
|
|
10
9
|
import { createReplTool } from "./tool/repl-tool.ts";
|
|
11
10
|
import { loadSettings, mergeConfig, resolveModelId } from "./config/settings.ts";
|
|
12
11
|
import { RlmController, cheapestModel } from "./mode/rlm-mode.ts";
|
|
@@ -17,27 +16,45 @@ import { packRepository, formatForLLM, serializeForSandbox } from "./context/rep
|
|
|
17
16
|
import { buildNativeSystemPrompt } from "./prompts/system.ts";
|
|
18
17
|
import { errorMessage } from "./util/errors.ts";
|
|
19
18
|
|
|
20
|
-
const BLOCKED_NATIVE_TOOLS = Object.freeze(new Set(["read", "grep"
|
|
19
|
+
const BLOCKED_NATIVE_TOOLS = Object.freeze(new Set(["read", "grep"]));
|
|
21
20
|
|
|
22
21
|
export default function rlmExtension(pi: ExtensionAPI): void {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
29
|
-
const persisted = await loadSettings();
|
|
30
|
-
const config = mergeConfig(persisted.config);
|
|
22
|
+
// Init synchronously with defaults — ensures commands/tools/handlers register before session_start
|
|
23
|
+
const config = mergeConfig({});
|
|
31
24
|
const controller = new RlmController(config);
|
|
32
|
-
controller.savedWorkerRef = persisted.worker;
|
|
33
|
-
|
|
34
|
-
// ── SandboxManager — persistent singleton for native-mode repl() ──
|
|
35
25
|
const sandboxManager = new SandboxManager({
|
|
36
26
|
execTimeoutS: config.execTimeoutS,
|
|
37
27
|
requestTimeoutMs: config.requestTimeoutMs,
|
|
38
28
|
python: config.python,
|
|
39
29
|
sandboxInitTimeoutMs: config.sandboxInitTimeoutMs,
|
|
40
30
|
});
|
|
31
|
+
let packedContextText: string | undefined;
|
|
32
|
+
let contextPackPromise: Promise<string | undefined> | undefined;
|
|
33
|
+
const ensureRepositoryContext = async (cwd: string): Promise<string | undefined> => {
|
|
34
|
+
if (packedContextText !== undefined && sandboxManager.contextPayload !== null) return packedContextText;
|
|
35
|
+
contextPackPromise ??= packRepository(cwd)
|
|
36
|
+
.then((result) => {
|
|
37
|
+
if (!result.ok) {
|
|
38
|
+
console.warn(`[rlm] repository context pack failed: ${result.error}`);
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
sandboxManager.contextPayload = serializeForSandbox(result.value);
|
|
42
|
+
packedContextText = formatForLLM(result.value);
|
|
43
|
+
return packedContextText;
|
|
44
|
+
})
|
|
45
|
+
.finally(() => { contextPackPromise = undefined; });
|
|
46
|
+
return contextPackPromise;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Load persisted settings async — applied before session_start handler reads controller state
|
|
50
|
+
const settingsReady = loadSettings()
|
|
51
|
+
.then((persisted) => {
|
|
52
|
+
controller.config = mergeConfig(persisted.config);
|
|
53
|
+
controller.savedWorkerRef = persisted.worker;
|
|
54
|
+
})
|
|
55
|
+
.catch((err) => {
|
|
56
|
+
console.warn(`[rlm] settings load failed: ${errorMessage(err)}`);
|
|
57
|
+
});
|
|
41
58
|
|
|
42
59
|
// ── Message renderers ──
|
|
43
60
|
pi.registerMessageRenderer(
|
|
@@ -56,23 +73,19 @@ async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
56
73
|
registerRlmConfigCommand(pi, controller);
|
|
57
74
|
|
|
58
75
|
// ── Tool registration ──
|
|
59
|
-
// Existing rlm tool (stays for backward compat with /rlm mode)
|
|
60
76
|
pi.registerTool(createRlmTool(controller));
|
|
61
|
-
pi.registerTool(createApplyDiffTool());
|
|
62
|
-
|
|
63
|
-
// Native repl tool — re-registered each session to pick up model provider changes
|
|
64
77
|
let guidePosted = false;
|
|
65
78
|
|
|
66
79
|
pi.on("session_start", async (_event, ctx) => {
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
// Wait for persisted settings before reading controller state
|
|
81
|
+
await settingsReady;
|
|
82
|
+
|
|
70
83
|
if (controller.savedWorkerRef) {
|
|
71
84
|
const resolved = resolveModelId(ctx.modelRegistry, controller.savedWorkerRef);
|
|
72
85
|
if (resolved) controller.workerModel = resolved;
|
|
73
86
|
}
|
|
74
87
|
|
|
75
|
-
//
|
|
88
|
+
// Re-register repl tool each session to pick up model provider changes
|
|
76
89
|
const workerModel = controller.workerModel ?? cheapestModel(ctx.modelRegistry) ?? ctx.model;
|
|
77
90
|
const model = ctx.model;
|
|
78
91
|
if (workerModel && model) {
|
|
@@ -84,7 +97,11 @@ async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
84
97
|
getModel: () => controller.resolveModels(ctx)?.model,
|
|
85
98
|
getWorkerModel: () => controller.resolveModels(ctx)?.worker,
|
|
86
99
|
registry: ctx.modelRegistry,
|
|
87
|
-
config,
|
|
100
|
+
config: controller.config,
|
|
101
|
+
ensureContext: async () => {
|
|
102
|
+
const contextText = await ensureRepositoryContext(ctx.cwd ?? process.cwd());
|
|
103
|
+
if (contextText === undefined) throw new Error("repository context could not be loaded into RLM sandbox");
|
|
104
|
+
},
|
|
88
105
|
}));
|
|
89
106
|
} catch { /* re-registration on provider change — ignore if already registered */ }
|
|
90
107
|
}
|
|
@@ -111,14 +128,13 @@ async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
111
128
|
|
|
112
129
|
// Inject repository context as a compact listing (once per session, only when RLM is enabled)
|
|
113
130
|
if (controller.enabled && !contextInjected) {
|
|
114
|
-
contextInjected = true;
|
|
115
131
|
const cwd = ctx.cwd ?? process.cwd();
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
|
|
132
|
+
const contextText = await ensureRepositoryContext(cwd);
|
|
133
|
+
if (contextText !== undefined) {
|
|
134
|
+
contextInjected = true;
|
|
119
135
|
const instruction = [
|
|
120
|
-
"ANALYZE THIS REPOSITORY using repl({code}) — read/grep
|
|
121
|
-
|
|
136
|
+
"ANALYZE THIS REPOSITORY using repl({code}) — read/grep are DISABLED.",
|
|
137
|
+
"Repository contents are pre-loaded in the Python REPL `context` variable.",
|
|
122
138
|
"Chunk context via Python, delegate to llm_query. If credits exhausted → report and stop.",
|
|
123
139
|
"",
|
|
124
140
|
].join("\n");
|
|
@@ -128,9 +144,6 @@ async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
128
144
|
timestamp: 0,
|
|
129
145
|
} as (typeof filtered)[number];
|
|
130
146
|
|
|
131
|
-
// Store context for sandbox loading on first repl() call
|
|
132
|
-
sandboxManager.contextPayload = serializeForSandbox(result.value);
|
|
133
|
-
|
|
134
147
|
return { messages: [contextMsg, ...filtered] };
|
|
135
148
|
}
|
|
136
149
|
}
|
|
@@ -139,19 +152,20 @@ async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
139
152
|
});
|
|
140
153
|
|
|
141
154
|
// ── Input routing: native mode — agent decides whether to use repl() or other tools ──
|
|
142
|
-
// The old black-box rlm() routing is removed; the main agent receives messages normally
|
|
143
|
-
// and chooses natively when to call repl(), read, grep, zebra-mcp, etc.
|
|
144
155
|
pi.on("input", async (_event, _ctx) => {
|
|
145
156
|
return { action: "continue" };
|
|
146
157
|
});
|
|
147
158
|
|
|
148
|
-
// ── Tool restriction: block
|
|
159
|
+
// ── Tool restriction: block analysis tools when RLM is ON ──
|
|
160
|
+
// `edit`/`write` stay unblocked so the agent modifies files through Pi's native
|
|
161
|
+
// tool flow (visible to all plugins, +/- diff preview). Only read/grep are
|
|
162
|
+
// blocked — the repository is pre-loaded in the REPL `context` variable.
|
|
149
163
|
pi.on("tool_call", async (event) => {
|
|
150
164
|
if (!controller.enabled) return;
|
|
151
165
|
if (BLOCKED_NATIVE_TOOLS.has(event.toolName)) {
|
|
152
166
|
return {
|
|
153
167
|
block: true,
|
|
154
|
-
reason: "RLM mode active. Use repl({code}) to read files and
|
|
168
|
+
reason: "RLM mode active. Use repl({code}) to read files and search the repository — all files are pre-loaded in the REPL `context` variable. Use `edit`/`write` for file changes. If sub-LLM credits are exhausted, report to the user.",
|
|
155
169
|
};
|
|
156
170
|
}
|
|
157
171
|
});
|
|
@@ -161,5 +175,8 @@ async function setupRlmExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
161
175
|
controller.abort();
|
|
162
176
|
await sandboxManager.dispose();
|
|
163
177
|
contextInjected = false;
|
|
178
|
+
packedContextText = undefined;
|
|
179
|
+
contextPackPromise = undefined;
|
|
180
|
+
sandboxManager.contextPayload = null;
|
|
164
181
|
});
|
|
165
182
|
}
|
package/src/prompts/system.ts
CHANGED
|
@@ -162,11 +162,13 @@ function nativeReplGlossary(): string {
|
|
|
162
162
|
"- `rlm_query(prompt, model=None) -> str` — recursive RLM with its own REPL for complex sub-tasks needing iterative reasoning.",
|
|
163
163
|
"- `rlm_query_batched(prompts, model=None) -> list[str]` — concurrent recursive RLM calls.",
|
|
164
164
|
"",
|
|
165
|
-
"**Choosing between `llm_query` and `rlm_query`:** default to `llm_query`
|
|
166
|
-
"and fan out with `llm_query_batched`; reach for `rlm_query` only when a sub-task needs its own iterative",
|
|
167
|
-
"reasoning. Avoid excessive recursive sub-calls when a batched one-shot suffices.",
|
|
165
|
+
"**Choosing between `llm_query` and `rlm_query`:** default to `llm_query`/batched; use `rlm_query` only for iterative sub-tasks.",
|
|
168
166
|
"- `todo(action, **kwargs) -> str` — manage a task list. Actions: create, update, list, get, delete, clear. Statuses: pending → in_progress → completed.",
|
|
169
167
|
"- `SHOW_VARS() -> str` — list all variables currently in the REPL.",
|
|
168
|
+
"- `stage_edit(path, old_text, new_text) -> str`: stage a file edit computed inside the REPL.",
|
|
169
|
+
" Read the file from `context`, compute the exact change in Python, then call",
|
|
170
|
+
" stage_edit once per file. The repl() result will include a STAGED_EDITS JSON block.",
|
|
171
|
+
" The main agent must then call `edit` for each entry verbatim — zero analysis needed.",
|
|
170
172
|
"- `answer`: dict `{\"content\": \"\", \"ready\": False}`. To submit: `answer[\"content\"] = \"...\"; answer[\"ready\"] = True`.",
|
|
171
173
|
"",
|
|
172
174
|
"### Orchestrator Pattern",
|
|
@@ -198,45 +200,23 @@ function nativeReplGlossary(): string {
|
|
|
198
200
|
"| `repl({code})` | Need to chunk/delegate `context` to sub-LLMs; need Python scripting; need REPL state across calls |",
|
|
199
201
|
"| `read` / `grep` | Inspect a few specific files directly; small codebase |",
|
|
200
202
|
"| `zebra-mcp` | Semantic search over the codebase |",
|
|
201
|
-
"| `
|
|
203
|
+
"| `edit` | Modify an existing file with exact text replacement (native Pi flow, visible to all plugins) |",
|
|
204
|
+
"| `write` | Create a new file (native Pi flow, visible to all plugins) |",
|
|
202
205
|
"| `llm_query` (inside repl) | Extract, summarize, or classify a chunk of text |",
|
|
203
206
|
"| `rlm_query` (inside repl) | Complex sub-task needing iterative reasoning with its own REPL |",
|
|
204
207
|
"| `todo` (inside repl) | Track multi-step progress visibly to the user |",
|
|
208
|
+
"| `stage_edit(path, old, new)` (inside repl) | Sub-agent stages exact edit params; relay STAGED_EDITS to `edit` |",
|
|
205
209
|
"",
|
|
206
210
|
"### Workflow",
|
|
207
211
|
"1. **Plan**: Create todos for the multi-step analysis. Probe `context` — print length, inspect a few entries.",
|
|
208
212
|
"2. **Chunk & Delegate**: Slice `context` into batches, delegate each batch to sub-LLMs via `llm_query_batched`.",
|
|
209
213
|
"3. **Aggregate**: Collect results in Python, pass aggregated results to a final `llm_query` or produce the answer directly.",
|
|
210
|
-
"4. **Finalize**:
|
|
214
|
+
"4. **Finalize**: For file changes, stage them inside repl() via stage_edit(path, old, new), then relay the STAGED_EDITS from the result to `edit`. For analysis tasks, write a normal message.",
|
|
211
215
|
"",
|
|
212
216
|
"### Task-Specific Patterns",
|
|
213
|
-
"",
|
|
214
|
-
"
|
|
215
|
-
"
|
|
216
|
-
"2. Chunk → split files into module batches (~10-15 files each)",
|
|
217
|
-
"3. **DELEGATE ALL** → `llm_query_batched` on EVERY module: \"Summarize each file's role, what it exports, and how it connects\". Send ALL batches.",
|
|
218
|
-
"4. Aggregate → collect all sub-LLM summaries, synthesize diagram from them.",
|
|
219
|
-
"5. If sub-LLM credits exhausted → report to user: \"Credits exhausted after N batches. Results so far: ...\"",
|
|
220
|
-
"",
|
|
221
|
-
"**Bug investigation / \"find the issue\"**:",
|
|
222
|
-
"1. `repl()` → grep context for keywords (use Python re/in operators)",
|
|
223
|
-
"2. `llm_query` on matching files: \"Is there a bug here? What could cause X?\"",
|
|
224
|
-
"",
|
|
225
|
-
"**Full code review / audit**:",
|
|
226
|
-
"1. `repl()` → chunk all files, delegate ALL to `llm_query_batched` with review criteria",
|
|
227
|
-
"2. Aggregate findings, report to user",
|
|
228
|
-
"",
|
|
229
|
-
"CRITICAL: Never read files directly. If sub-LLMs fail → report, don't fall back to read.",
|
|
230
|
-
"",
|
|
231
|
-
"### Handling Sub-LLM Failures",
|
|
232
|
-
"Sub-LLM calls can fail (credit limits, rate limits, timeouts). Handle gracefully:",
|
|
233
|
-
"",
|
|
234
|
-
"| Failure | Action |",
|
|
235
|
-
"|---------|--------|",
|
|
236
|
-
"| `llm_query_batched` all fail | Reduce batch size (try 3-5 instead of 10+). If still failing, use individual `llm_query` calls. |",
|
|
237
|
-
"| Individual `llm_query` fails | Check error message. If credit/rate-limit, wait and retry once. If still failing, read files directly with `read`/`grep`. |",
|
|
238
|
-
"| `rlm_query` fails | Fall back to `llm_query` — it's a one-shot call that uses fewer resources. |",
|
|
239
|
-
"| All sub-LLMs exhausted | Read key files directly. For small repos (<20 files), direct reading is fine. For large repos, prioritize the most important files. |",
|
|
217
|
+
"- Architecture/code review: chunk relevant files and delegate summaries or review to `llm_query_batched`.",
|
|
218
|
+
"- Bug investigation: use Python string/regex search over `context`; delegate matching files for analysis.",
|
|
219
|
+
"- If sub-LLM credits are exhausted, report partial results and stop — do not bypass REPL restrictions.",
|
|
240
220
|
"",
|
|
241
221
|
"Reserve your own tokens for high-level decisions: what to ask next, how to combine sub-LLM outputs, when to finalize.",
|
|
242
222
|
"Delegate everything else. Do not submit a final answer before inspecting `context`.",
|
|
@@ -250,14 +230,16 @@ export function buildNativeSystemPrompt(): string {
|
|
|
250
230
|
"║ NATIVE RLM MODE — YOU ARE AN ORCHESTRATOR, NOT A READER ║",
|
|
251
231
|
"╚══════════════════════════════════════════════════════════════════╝",
|
|
252
232
|
"",
|
|
253
|
-
"ABSOLUTE RESTRICTION: Do NOT use `read
|
|
233
|
+
"ABSOLUTE RESTRICTION: Do NOT use `read` or `grep` to access files.",
|
|
254
234
|
"All file content is pre-loaded in the REPL `context` variable. Use ONLY `repl({code})`.",
|
|
255
|
-
"You may read at most 2 hub files directly (README.md, package.json) for quick orientation.",
|
|
256
235
|
"If sub-LLM credits are exhausted → report the error to the user and stop.",
|
|
257
236
|
"",
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
"
|
|
237
|
+
"For file changes, use `edit` (modify existing) or `write` (create new) — these route through",
|
|
238
|
+
"Pi's native tool flow, visible to all plugins with a `+/-` diff preview.",
|
|
239
|
+
"",
|
|
240
|
+
"When repl() returns a STAGED_EDITS block, apply each entry by calling `edit` verbatim:",
|
|
241
|
+
" edit({ path: entry.path, edits: [{ oldText: entry.oldText, newText: entry.newText }] })",
|
|
242
|
+
"Do not analyze or modify the parameters — relay them exactly as provided by the sub-agent.",
|
|
261
243
|
"",
|
|
262
244
|
nativeReplGlossary(),
|
|
263
245
|
].join("\n");
|
package/src/sandbox/protocol.ts
CHANGED
|
@@ -32,10 +32,6 @@ export interface ProposedEdit {
|
|
|
32
32
|
readonly newText: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export interface ProposedDiffEdit {
|
|
36
|
-
readonly diff: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
35
|
/** A normal response to a request (keyed by the request `id`). */
|
|
40
36
|
export interface WorkerResponse {
|
|
41
37
|
readonly id: string;
|
|
@@ -47,7 +43,6 @@ export interface WorkerResponse {
|
|
|
47
43
|
readonly final_answer?: string | null;
|
|
48
44
|
readonly answer_content?: string;
|
|
49
45
|
readonly edits?: readonly ProposedEdit[];
|
|
50
|
-
readonly diffs?: readonly ProposedDiffEdit[];
|
|
51
46
|
readonly raised?: boolean;
|
|
52
47
|
readonly execution_time?: number;
|
|
53
48
|
// user-created variable names after this exec (filters builtins/context) — Metadata(stdout) for history orientation
|
|
@@ -183,7 +178,6 @@ export interface ReplResult {
|
|
|
183
178
|
readonly finalAnswer: string | null;
|
|
184
179
|
readonly answerContent: string;
|
|
185
180
|
readonly edits: readonly ProposedEdit[];
|
|
186
|
-
readonly diffs: readonly ProposedDiffEdit[];
|
|
187
181
|
readonly raised: boolean;
|
|
188
182
|
readonly executionTimeMs: number;
|
|
189
183
|
/** User-created variable names after this exec (builtins/context filtered out). */
|
|
@@ -35,8 +35,7 @@ export class SandboxManager {
|
|
|
35
35
|
* given handlers. Subsequent calls return the existing sandbox immediately.
|
|
36
36
|
* Deduplicates concurrent calls via initPromise.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
* before first use.
|
|
38
|
+
* If contextPayload is set, it is loaded before the sandbox is returned.
|
|
40
39
|
*/
|
|
41
40
|
async getOrCreate(handlers: Partial<SubLlmHandlers>): Promise<PythonSandbox> {
|
|
42
41
|
if (this.disposed) throw new Error("SandboxManager disposed");
|
|
@@ -45,8 +44,8 @@ export class SandboxManager {
|
|
|
45
44
|
// "context" event's async packRepository resolves after the first repl() call).
|
|
46
45
|
// Load it into the live sandbox now if still pending.
|
|
47
46
|
if (this.contextPayload !== null && !this.contextLoaded) {
|
|
47
|
+
await this.sandbox.loadContext(this.contextPayload);
|
|
48
48
|
this.contextLoaded = true;
|
|
49
|
-
try { await this.sandbox.loadContext(this.contextPayload); } catch { /* best-effort */ }
|
|
50
49
|
}
|
|
51
50
|
return this.sandbox;
|
|
52
51
|
}
|
|
@@ -61,15 +60,16 @@ export class SandboxManager {
|
|
|
61
60
|
initTimeoutMs: this.config.sandboxInitTimeoutMs,
|
|
62
61
|
handlers,
|
|
63
62
|
}).then(async (s) => {
|
|
64
|
-
// Load context on first creation if available
|
|
63
|
+
// Load context on first creation if available.
|
|
65
64
|
if (this.contextPayload !== null) {
|
|
65
|
+
await s.loadContext(this.contextPayload);
|
|
66
66
|
this.contextLoaded = true;
|
|
67
|
-
try { await s.loadContext(this.contextPayload); } catch { /* best-effort */ }
|
|
68
67
|
}
|
|
69
68
|
this.sandbox = s;
|
|
70
69
|
this.initPromise = null;
|
|
71
70
|
return s;
|
|
72
71
|
}).catch((err) => {
|
|
72
|
+
this.contextLoaded = false;
|
|
73
73
|
this.initPromise = null;
|
|
74
74
|
throw err;
|
|
75
75
|
});
|
|
@@ -115,6 +115,7 @@ export class SandboxManager {
|
|
|
115
115
|
// Best-effort dispose of the dead sandbox
|
|
116
116
|
try { await this.sandbox.dispose(); } catch { /* already dead */ }
|
|
117
117
|
this.sandbox = null;
|
|
118
|
+
this.contextLoaded = false;
|
|
118
119
|
}
|
|
119
120
|
throw err;
|
|
120
121
|
} finally {
|
|
@@ -139,5 +140,6 @@ export class SandboxManager {
|
|
|
139
140
|
this.disposed = true;
|
|
140
141
|
await this.sandbox?.dispose();
|
|
141
142
|
this.sandbox = null;
|
|
143
|
+
this.contextLoaded = false;
|
|
142
144
|
}
|
|
143
145
|
}
|
package/src/sandbox/sandbox.ts
CHANGED
|
@@ -161,6 +161,7 @@ export class PythonSandbox {
|
|
|
161
161
|
try {
|
|
162
162
|
path = await this.writeContextFile(payload, isJson);
|
|
163
163
|
const res = await this.request({ type: "load_context", path, index, json: isJson });
|
|
164
|
+
if (!res.ok) throw new Error(res.error ?? "load_context failed");
|
|
164
165
|
return res.index ?? 0;
|
|
165
166
|
} finally {
|
|
166
167
|
if (path) await unlink(path).catch(() => {});
|
|
@@ -183,13 +184,13 @@ export class PythonSandbox {
|
|
|
183
184
|
|
|
184
185
|
async exec(code: string): Promise<ReplResult> {
|
|
185
186
|
const res = await this.request({ type: "exec", code });
|
|
187
|
+
if (!res.ok) throw new Error(res.error ?? "exec failed");
|
|
186
188
|
return {
|
|
187
189
|
stdout: res.stdout ?? "",
|
|
188
190
|
stderr: res.stderr ?? "",
|
|
189
191
|
finalAnswer: res.final_answer ?? null,
|
|
190
192
|
answerContent: res.answer_content ?? "",
|
|
191
193
|
edits: res.edits ?? [],
|
|
192
|
-
diffs: res.diffs ?? [],
|
|
193
194
|
raised: res.raised ?? false,
|
|
194
195
|
executionTimeMs: Math.round((res.execution_time ?? 0) * 1000),
|
|
195
196
|
varNames: res.var_names ?? [],
|
|
@@ -213,6 +214,7 @@ export class PythonSandbox {
|
|
|
213
214
|
async snapshot(path: string, nonce: string): Promise<boolean> {
|
|
214
215
|
try {
|
|
215
216
|
const res = await this.request({ type: "snapshot", path, nonce });
|
|
217
|
+
if (!res.ok) return false;
|
|
216
218
|
return res.ok;
|
|
217
219
|
} catch {
|
|
218
220
|
return false;
|
|
@@ -223,6 +225,7 @@ export class PythonSandbox {
|
|
|
223
225
|
async restore(path: string, nonce: string): Promise<boolean> {
|
|
224
226
|
try {
|
|
225
227
|
const res = await this.request({ type: "restore", path, nonce });
|
|
228
|
+
if (!res.ok) return false;
|
|
226
229
|
return res.ok;
|
|
227
230
|
} catch {
|
|
228
231
|
return false;
|
package/src/sandbox/worker.py
CHANGED
|
@@ -65,7 +65,8 @@ RESERVED = frozenset(
|
|
|
65
65
|
"llm_query", "llm_query_batched", "rlm_query", "rlm_query_batched",
|
|
66
66
|
"advance_phase",
|
|
67
67
|
"ask_user_question", "todo",
|
|
68
|
-
"
|
|
68
|
+
"stage_edit",
|
|
69
|
+
"SHOW_VARS", "answer", "context",
|
|
69
70
|
}
|
|
70
71
|
)
|
|
71
72
|
|
|
@@ -103,6 +104,7 @@ class Worker:
|
|
|
103
104
|
def _setup(self) -> None:
|
|
104
105
|
self.ns = {"__builtins__": _SAFE_BUILTINS.copy(), "__name__": "__main__"}
|
|
105
106
|
self._ctx_payloads: dict[int, Any] = {}
|
|
107
|
+
self._staged_edits: list[dict[str, str]] = []
|
|
106
108
|
self._restore_scaffold()
|
|
107
109
|
|
|
108
110
|
def _capture_answer(self, content: Any) -> None:
|
|
@@ -118,8 +120,7 @@ class Worker:
|
|
|
118
120
|
ns["advance_phase"] = self._advance_phase
|
|
119
121
|
ns["ask_user_question"] = self._ask_user_question
|
|
120
122
|
ns["todo"] = self._todo
|
|
121
|
-
ns["
|
|
122
|
-
ns["SHOW_DIFFS"] = self._show_diffs
|
|
123
|
+
ns["stage_edit"] = self._stage_edit
|
|
123
124
|
ns["SHOW_VARS"] = self._show_vars
|
|
124
125
|
if not isinstance(ns.get("answer"), _AnswerDict):
|
|
125
126
|
cur = ns.get("answer")
|
|
@@ -153,12 +154,6 @@ class Worker:
|
|
|
153
154
|
avail = {k: type(self.ns[k]).__name__ for k in self._user_var_names()}
|
|
154
155
|
return f"Available variables: {avail}" if avail else "No variables created yet."
|
|
155
156
|
|
|
156
|
-
def _show_edits(self) -> str:
|
|
157
|
-
return "No edits — edit tools are not available in this run."
|
|
158
|
-
|
|
159
|
-
def _show_diffs(self) -> str:
|
|
160
|
-
return "No diffs — edit tools are not available in this run."
|
|
161
|
-
|
|
162
157
|
# ---- sub-LLM bridge over stdio --------------------------------------------------------
|
|
163
158
|
|
|
164
159
|
def _rpc(self, kind: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -263,6 +258,12 @@ class Worker:
|
|
|
263
258
|
return f"Error: {r['error']}"
|
|
264
259
|
return str(r.get("response", "ok"))
|
|
265
260
|
|
|
261
|
+
def _stage_edit(self, path: str, old_text: str, new_text: str) -> str:
|
|
262
|
+
if not isinstance(path, str) or not isinstance(old_text, str) or not isinstance(new_text, str):
|
|
263
|
+
return "Error: path, old_text, new_text must be strings"
|
|
264
|
+
self._staged_edits.append({"path": path, "oldText": old_text, "newText": new_text})
|
|
265
|
+
return f"Staged edit for {path} ({len(old_text)} → {len(new_text)} chars)"
|
|
266
|
+
|
|
266
267
|
def _advance_phase(self, phase: str, summary: str | None = None) -> str:
|
|
267
268
|
"""Transition the root RLM pipeline to a new phase.
|
|
268
269
|
|
|
@@ -348,6 +349,7 @@ class Worker:
|
|
|
348
349
|
stdout = out.getvalue()
|
|
349
350
|
stderr = err.getvalue() + f"\n{type(e).__name__}: {e}\n" + traceback.format_exc()
|
|
350
351
|
final, self._final_answer = self._final_answer, None
|
|
352
|
+
edits, self._staged_edits = self._staged_edits, []
|
|
351
353
|
answer = self.ns.get("answer")
|
|
352
354
|
answer_content = answer.get("content", "") if isinstance(answer, dict) else ""
|
|
353
355
|
return {
|
|
@@ -355,8 +357,7 @@ class Worker:
|
|
|
355
357
|
"stderr": stderr,
|
|
356
358
|
"final_answer": final,
|
|
357
359
|
"answer_content": str(answer_content),
|
|
358
|
-
"edits":
|
|
359
|
-
"diffs": [],
|
|
360
|
+
"edits": edits,
|
|
360
361
|
"raised": raised,
|
|
361
362
|
"execution_time": time.perf_counter() - start,
|
|
362
363
|
"var_names": self._user_var_names(),
|
package/src/tool/repl-details.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* accumulated into the subcalls array for tree rendering.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { ProposedEdit } from "../sandbox/protocol.ts";
|
|
9
10
|
import type { RlmSubcall } from "./rlm-details.ts";
|
|
10
11
|
|
|
11
12
|
export interface ReplDetails {
|
|
@@ -20,4 +21,6 @@ export interface ReplDetails {
|
|
|
20
21
|
readonly subcalls: readonly RlmSubcall[];
|
|
21
22
|
/** Running totals for this repl() call (cost + tokens from sub-LLM calls). */
|
|
22
23
|
readonly totals: { readonly costUsd: number; readonly tokens: number };
|
|
24
|
+
/** File edits staged inside the REPL for native relay through edit(). */
|
|
25
|
+
readonly edits?: readonly ProposedEdit[];
|
|
23
26
|
}
|