@hicaru/pi-rlm 0.3.1 → 0.3.3
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 +131 -154
- package/README.ru.md +5 -5
- package/README.zh-CN.md +5 -5
- package/package.json +3 -2
- package/src/bridge/handlers/await.ts +148 -0
- package/src/bridge/handlers/completion.ts +72 -0
- package/src/bridge/handlers/emitting.ts +104 -0
- package/src/bridge/handlers/finish.ts +45 -0
- package/src/bridge/handlers/index.ts +48 -0
- package/src/bridge/handlers/llm-query.ts +130 -0
- package/src/bridge/handlers/rlm-query.ts +227 -0
- package/src/bridge/handlers/task-registry.ts +202 -0
- package/src/bridge/handlers/types.ts +136 -0
- package/src/commands/rlm-config.ts +40 -12
- package/src/config/settings.ts +13 -3
- package/src/context/listing.ts +2 -2
- package/src/context/refresh.ts +141 -0
- package/src/core/engine.ts +16 -18
- package/src/core/types.ts +1 -3
- package/src/index.ts +59 -54
- package/src/mode/native-guards.ts +4 -4
- package/src/mode/rlm-mode.ts +6 -1
- package/src/mode/subagent.ts +1 -1
- package/src/prompts/glossary.ts +71 -74
- package/src/prompts/native.ts +127 -85
- package/src/prompts/system.ts +29 -15
- package/src/sandbox/interrupts.ts +258 -68
- package/src/sandbox/protocol.ts +53 -30
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/hostio.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +8 -5
- package/src/sandbox/py/retrieval.py +17 -8
- package/src/sandbox/py/tasks.py +1 -1
- package/src/sandbox/py/worker.py +106 -79
- package/src/sandbox/sandbox-manager.ts +26 -1
- package/src/sandbox/sandbox.ts +1 -1
- package/src/tool/background-tasks.ts +1 -1
- package/src/tool/repl-result.ts +2 -2
- package/src/tool/repl-tool.ts +13 -14
- package/src/ui/config-panel.ts +1 -1
- package/src/ui/intro.ts +1 -4
- package/src/ui/model-picker.ts +32 -2
- package/src/util/concurrency.ts +1 -1
- package/src/bridge/subcall-handlers.ts +0 -382
package/src/core/types.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface RlmConfig {
|
|
|
55
55
|
* Keeps each turn short so the next turn's input stays manageable.
|
|
56
56
|
* `reasoning` is read from `smartReasoning` if omitted here. */
|
|
57
57
|
readonly rootSampling?: Readonly<Sampling>;
|
|
58
|
-
/** System prompt injected into every llm_query /
|
|
58
|
+
/** System prompt injected into every llm_query / llm_batch sub-call.
|
|
59
59
|
* Instructs the worker model to respond concisely.
|
|
60
60
|
* undefined = no system prompt (raw completion). */
|
|
61
61
|
readonly subSystemPrompt?: string;
|
|
@@ -73,8 +73,6 @@ export interface RlmInput {
|
|
|
73
73
|
readonly depth: number;
|
|
74
74
|
/** AgentTree node to attach this run's node under (set when recursing). */
|
|
75
75
|
readonly parentNodeId?: string;
|
|
76
|
-
/** "provider/id" — overrides the root model for this run (set by recursive rlm_query). */
|
|
77
|
-
readonly modelOverride?: string;
|
|
78
76
|
/** Remaining timeout for this subtree (set by parent from its LimitGuard). */
|
|
79
77
|
readonly remainingTimeoutMs?: number;
|
|
80
78
|
}
|
package/src/index.ts
CHANGED
|
@@ -18,9 +18,10 @@ import { BackgroundTasks } from "./tool/background-tasks.ts";
|
|
|
18
18
|
import { resolve } from "node:path";
|
|
19
19
|
import { resolveSource } from "./context/resolve.ts";
|
|
20
20
|
import { formatContextListing } from "./context/listing.ts";
|
|
21
|
+
import { extractEditPaths, readDiskFile } from "./context/refresh.ts";
|
|
21
22
|
import type { AddContextHandlerBundle } from "./bridge/add-context.ts";
|
|
22
|
-
import { buildNativeSystemPrompt
|
|
23
|
-
import {
|
|
23
|
+
import { buildNativeSystemPrompt } from "./prompts/native.ts";
|
|
24
|
+
import { capToolResultText } from "./mode/native-guards.ts";
|
|
24
25
|
import {
|
|
25
26
|
isSubagentChildBypass,
|
|
26
27
|
commitSubagentForceActivation,
|
|
@@ -37,14 +38,13 @@ export {
|
|
|
37
38
|
processRlmDepth,
|
|
38
39
|
} from "./mode/subagent.ts";
|
|
39
40
|
|
|
40
|
-
const BLOCKED_NATIVE_TOOLS = Object.freeze(new Set(["read", "grep"]));
|
|
41
41
|
/** How often to keep the parent sandbox's request watchdog alive during detached work. */
|
|
42
42
|
const WATCHDOG_HEARTBEAT_MS = 30_000;
|
|
43
|
-
|
|
43
|
+
/** Soft token guard — cap bulk tool stdout; do NOT hard-block read/grep/bash readers. */
|
|
44
|
+
const CAPPED_RESULT_TOOLS = Object.freeze(new Set(["bash", "find", "ls", "read", "grep"]));
|
|
44
45
|
|
|
45
46
|
export default function rlmExtension(pi: ExtensionAPI): void {
|
|
46
|
-
// Subagent children
|
|
47
|
-
// (block read/grep, route through repl). Env fast path: full bypass when
|
|
47
|
+
// Subagent children may bypass full RLM registration. Env fast path when
|
|
48
48
|
// PI_SUBAGENT_CHILD=1 (unless force-in under the depth cap). See mode/subagent.ts.
|
|
49
49
|
if (isSubagentChildBypass()) {
|
|
50
50
|
if (traceEnabled) {
|
|
@@ -124,16 +124,6 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
124
124
|
await seedPromise;
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
-
// Load persisted settings async — applied before session_start handler reads controller state
|
|
128
|
-
const settingsReady = loadSettings()
|
|
129
|
-
.then((persisted) => {
|
|
130
|
-
controller.config = mergeConfig(persisted.config);
|
|
131
|
-
controller.savedLlmRef = persisted.llm;
|
|
132
|
-
})
|
|
133
|
-
.catch((err) => {
|
|
134
|
-
console.warn(`[rlm] settings load failed: ${errorMessage(err)}`);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
127
|
// ── Message renderers ──
|
|
138
128
|
// Markdown themes are derived from the injected `theme`, never pi's module-global
|
|
139
129
|
// `getMarkdownTheme()` — under jiti that global can be undefined inside a plugin.
|
|
@@ -162,8 +152,11 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
162
152
|
let guidePosted = false;
|
|
163
153
|
|
|
164
154
|
pi.on("session_start", async (_event, ctx) => {
|
|
165
|
-
//
|
|
166
|
-
|
|
155
|
+
// Re-read settings fresh from disk each session so a pin or config change
|
|
156
|
+
// made during a previous session takes effect.
|
|
157
|
+
const persisted = await loadSettings();
|
|
158
|
+
controller.config = mergeConfig(persisted.config);
|
|
159
|
+
controller.savedLlmRef = persisted.llm ?? undefined;
|
|
167
160
|
|
|
168
161
|
// An explicit --rlm flag wins over the persisted setting for this session.
|
|
169
162
|
const flag = pi.getFlag("rlm");
|
|
@@ -182,7 +175,23 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
182
175
|
|
|
183
176
|
if (controller.savedLlmRef) {
|
|
184
177
|
const resolved = resolveModelId(ctx.modelRegistry, controller.savedLlmRef);
|
|
185
|
-
if (resolved)
|
|
178
|
+
if (resolved) {
|
|
179
|
+
controller.llmModel = resolved;
|
|
180
|
+
} else {
|
|
181
|
+
// Keep the pin on disk/controller — do not fall back permanently. Runtime uses
|
|
182
|
+
// cheapest until the catalog has the model again; surface that once per session.
|
|
183
|
+
console.warn(
|
|
184
|
+
`[rlm] pinned sub-LLM ${controller.savedLlmRef} not in registry; using cheapest until it reappears`,
|
|
185
|
+
);
|
|
186
|
+
try {
|
|
187
|
+
ctx.ui.notify(
|
|
188
|
+
`RLM: pinned llm=${controller.savedLlmRef} unavailable — using cheapest until it is`,
|
|
189
|
+
"warning",
|
|
190
|
+
);
|
|
191
|
+
} catch {
|
|
192
|
+
// Some hosts have no UI at session_start.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
186
195
|
}
|
|
187
196
|
|
|
188
197
|
// Re-register repl tool each session to pick up model provider changes
|
|
@@ -254,7 +263,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
254
263
|
const filtered = event.messages.filter(
|
|
255
264
|
(message) =>
|
|
256
265
|
!(message.role === "custom" && message.customType === "rlm-intro")
|
|
257
|
-
|
|
266
|
+
|
|
258
267
|
);
|
|
259
268
|
if (!nativeTradeHolds()) return { messages: filtered };
|
|
260
269
|
|
|
@@ -266,11 +275,11 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
266
275
|
listingPayloadRef = payload;
|
|
267
276
|
const listing = formatContextListing(payload);
|
|
268
277
|
const instruction = [
|
|
269
|
-
"
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
278
|
+
"Prefer repl({code}) for bulk analysis: free search/grep/outline, then fan-out Tasks.",
|
|
279
|
+
"Multi-module work → rlm_batch (or rlm_query); one-shot extracts → map_files/llm_batch.",
|
|
280
|
+
"Always-spawn returns Task (↯bg); only await_task has content — fire-all then await.",
|
|
281
|
+
"Large tool/repl outputs are capped. Files live in REPL `context` (cwd seeds first repl()).",
|
|
282
|
+
"add_context(path) for external dirs/files/docs/git. Credits exhausted → report and stop.",
|
|
274
283
|
"",
|
|
275
284
|
].join("\n");
|
|
276
285
|
filtered.unshift({
|
|
@@ -280,42 +289,38 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
280
289
|
} as PiMessage);
|
|
281
290
|
}
|
|
282
291
|
|
|
283
|
-
// Per-turn last-position reminder (not persisted — context hook rebuilds every request)
|
|
284
|
-
filtered.push({
|
|
285
|
-
role: "user" as const,
|
|
286
|
-
content: NATIVE_TURN_REMINDER,
|
|
287
|
-
timestamp: 0,
|
|
288
|
-
} as PiMessage);
|
|
289
292
|
|
|
290
293
|
return { messages: filtered };
|
|
291
294
|
});
|
|
292
295
|
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
296
|
+
// Soft token guard only — never hard-block read/grep/bash. Large tool results are capped.
|
|
297
|
+
// After edit/write, re-read disk into RLM context so search/llm see fresh content.
|
|
298
|
+
const MUTATING_FILE_TOOLS = Object.freeze(new Set(["edit", "write"]));
|
|
299
|
+
|
|
300
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
301
|
+
// ── Keep RLM context fresh after native file mutations ──
|
|
302
|
+
if (
|
|
303
|
+
nativeTradeHolds()
|
|
304
|
+
&& MUTATING_FILE_TOOLS.has(event.toolName)
|
|
305
|
+
&& event.isError !== true
|
|
306
|
+
) {
|
|
307
|
+
const cwd = resolve(ctx?.cwd ?? process.cwd());
|
|
308
|
+
const paths = extractEditPaths(event.input);
|
|
309
|
+
for (const p of paths) {
|
|
310
|
+
const body = await readDiskFile(p, cwd);
|
|
311
|
+
if (body === null) continue;
|
|
312
|
+
try {
|
|
313
|
+
await sandboxManager.refreshFileFromDisk(p, body, cwd);
|
|
314
|
+
// Listing must re-inject if we rewrote payload identity
|
|
315
|
+
listingPayloadRef = undefined;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (traceEnabled) {
|
|
318
|
+
trace("context.refresh_fail", { path: p, error: errorMessage(err) });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
303
321
|
}
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (BLOCKED_NATIVE_TOOLS.has(event.toolName)) {
|
|
307
|
-
return {
|
|
308
|
-
block: true,
|
|
309
|
-
reason: "RLM mode active. Use repl({code}) to read files and search the repository — loaded files live in the REPL `context` variable (cwd seeds on first call). Use `edit`/`write` for file changes. If sub-LLM credits are exhausted, report to the user.",
|
|
310
|
-
};
|
|
311
322
|
}
|
|
312
|
-
const bashCommand = event.toolName === "bash" ? bashCommandFromInput(event.input) : undefined;
|
|
313
|
-
if (bashCommand !== undefined && isFileReadingCommand(bashCommand)) {
|
|
314
|
-
return { block: true, reason: BASH_BLOCK_REASON };
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
323
|
|
|
318
|
-
pi.on("tool_result", async (event) => {
|
|
319
324
|
if (!nativeTradeHolds() || !CAPPED_RESULT_TOOLS.has(event.toolName)) return;
|
|
320
325
|
let changed = false;
|
|
321
326
|
const content = event.content.map((c) => {
|
|
@@ -43,7 +43,7 @@ export function isFileReadingCommand(command: string): boolean {
|
|
|
43
43
|
export const BASH_BLOCK_REASON =
|
|
44
44
|
"RLM mode: reading files via bash is blocked — that dumps file content into the root model's " +
|
|
45
45
|
"context. All files are pre-loaded in the REPL `context` variable: use repl({code}) with Python " +
|
|
46
|
-
"string/regex search, and delegate bulk analysis to llm_query /
|
|
46
|
+
"string/regex search, and delegate bulk analysis to llm_query / llm_batch / " +
|
|
47
47
|
"llm_query_chunked. bash is for RUNNING things (tests, builds, git).";
|
|
48
48
|
|
|
49
49
|
/** Max chars of tool output forwarded to the root model (≈1K tokens). */
|
|
@@ -51,12 +51,12 @@ export const TOOL_RESULT_CAP = 4_000;
|
|
|
51
51
|
|
|
52
52
|
const CAP_NOTE =
|
|
53
53
|
`\n[RLM: tool output capped at ${TOOL_RESULT_CAP.toLocaleString()} chars to protect the root ` +
|
|
54
|
-
"model's context — route bulk text through repl() + llm_query_chunked /
|
|
54
|
+
"model's context — route bulk text through repl() + llm_query_chunked / llm_batch.]";
|
|
55
55
|
|
|
56
56
|
const REPL_CAP_NOTE =
|
|
57
57
|
`\n[RLM: repl() stdout capped at ${TOOL_RESULT_CAP.toLocaleString()} chars — printing bulk text ` +
|
|
58
58
|
"is useless. Keep results in REPL variables and delegate semantic reading to llm_query / " +
|
|
59
|
-
"
|
|
59
|
+
"llm_batch / llm_query_chunked.]";
|
|
60
60
|
|
|
61
61
|
/** Shared truncation core. Returns undefined when under the cap (leave the text untouched). */
|
|
62
62
|
function capText(text: string, note: string): string | undefined {
|
|
@@ -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
|
-
"if you were READING, delegate via llm_query /
|
|
89
|
+
"if you were READING, delegate via llm_query / llm_batch / llm_query_chunked. " +
|
|
90
90
|
"Authoring an edit body yourself is correct and needs no delegation.]"
|
|
91
91
|
);
|
|
92
92
|
}
|
package/src/mode/rlm-mode.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface StartInput {
|
|
|
30
30
|
export class RlmController {
|
|
31
31
|
llmModel: Model<Api> | undefined;
|
|
32
32
|
savedLlmRef: string | undefined;
|
|
33
|
+
/** Set by applyLlmSelection when the user explicitly picks "cheapest (auto)". */
|
|
34
|
+
explicitClearPin = false;
|
|
33
35
|
private active: AbortController | null = null;
|
|
34
36
|
|
|
35
37
|
constructor(public config: RlmConfig) {}
|
|
@@ -58,7 +60,10 @@ export class RlmController {
|
|
|
58
60
|
async persist(): Promise<boolean> {
|
|
59
61
|
return await saveSettings({
|
|
60
62
|
config: this.config,
|
|
61
|
-
|
|
63
|
+
// null → explicit clear; undefined → merge from disk; string → set pin
|
|
64
|
+
llm: this.explicitClearPin
|
|
65
|
+
? null
|
|
66
|
+
: (modelRef(this.llmModel) ?? this.savedLlmRef),
|
|
62
67
|
});
|
|
63
68
|
}
|
|
64
69
|
|
package/src/mode/subagent.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 2. Capability gate — never confiscate native readers unless `repl` is in the
|
|
7
7
|
* active tool set (paper trade: scaffold only if the REPL substitute exists).
|
|
8
8
|
*
|
|
9
|
-
* In-process rlm_query depth is handled by
|
|
9
|
+
* In-process rlm_query depth is handled by bridge/handlers childRun; this module
|
|
10
10
|
* only covers OS-process children (pi subagents), which restart at depth 0.
|
|
11
11
|
*/
|
|
12
12
|
import { DEFAULT_CONFIG } from "../config/defaults.ts";
|
package/src/prompts/glossary.ts
CHANGED
|
@@ -29,10 +29,10 @@ export function promptCapTokensK(maxPromptChars: number): number {
|
|
|
29
29
|
*/
|
|
30
30
|
export const RETRIEVAL_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
31
31
|
"- `search(query: str, k=10, path_glob=None)`: BM25 ranking over `context`. Returns",
|
|
32
|
-
" [{path, line, score, snippet}] — POINTERS, not bodies
|
|
33
|
-
" no sub-LLM call
|
|
32
|
+
" [{path, line, score, snippet, text}] — POINTERS, not bodies (`text` aliases `snippet`).",
|
|
33
|
+
" **Start here.** Free: no sub-LLM call. Use before guessing filenames.",
|
|
34
34
|
"- `grep_context(pattern, k=50, path_glob=None, before=0, after=0) -> dict`: regex over",
|
|
35
|
-
" `context`. Returns {hits: [{path, line, text}], counts
|
|
35
|
+
" `context`. Returns {hits: [{path, line, text, snippet}], counts, total, truncated} —",
|
|
36
36
|
" `counts` is complete even when `hits` is capped, so a wide pattern reports its shape",
|
|
37
37
|
" instead of flooding you. Use for exact lexical needles; use `search` for meaning.",
|
|
38
38
|
"- `outline(path) -> str`: definition/heading skeleton of one file with line numbers.",
|
|
@@ -41,37 +41,38 @@ export const RETRIEVAL_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
41
41
|
|
|
42
42
|
/** One-line delegation helpers — orchestrating must be cheaper than solving. */
|
|
43
43
|
export const DELEGATION_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
44
|
-
"- `map_files(files, prompt
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
" one batch, then reduce the partial answers with a single call. The paper's canonical",
|
|
50
|
-
" strategy (query per chunk → aggregate the buffers) as one call.",
|
|
44
|
+
"- `map_files(files, prompt) -> Task`: always spawn. `await_task(t)` → dict[path, answer].",
|
|
45
|
+
" Accepts context entries or paths; packs into cap-sized batches; splits oversized files.",
|
|
46
|
+
" **Default way to read many files** — fire independent `map_files` Tasks, free work, then await.",
|
|
47
|
+
"- `llm_map_reduce(items, map_prompt, reduce_prompt) -> str`: **blocks** (map then reduce).",
|
|
48
|
+
" Prefer separate `map_files` / `llm_batch` Tasks when you can do free work between fan-out and collect.",
|
|
51
49
|
]);
|
|
52
50
|
|
|
53
51
|
/** Shared glossary entry for the chunked-query helper (headless + native). */
|
|
54
52
|
export const CHUNKED_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
55
|
-
"- `llm_query_chunked(text: str, prompt: str
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
" you open()ed, an oversized sub-result, or several concatenated context files.",
|
|
53
|
+
"- `llm_query_chunked(text: str, prompt: str) -> Task`: always spawn. `await_task(t)` → list[str]",
|
|
54
|
+
" (one answer per chunk, order preserved). Auto-splits text to the sub-LLM prompt cap.",
|
|
55
|
+
" Use for ANY text too large for a single `llm_query` — open()ed files, oversized sub-results.",
|
|
59
56
|
]);
|
|
60
57
|
|
|
61
58
|
/** Non-blocking fan-out: spawn now, collect later (headless glossary). */
|
|
62
59
|
export const SPAWN_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
63
|
-
"-
|
|
64
|
-
" `map_files
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
60
|
+
"- **ALWAYS SPAWN (Task + ↯bg):** `llm_query` / `llm_batch` / `rlm_query` / `rlm_batch` /",
|
|
61
|
+
" `map_files` / `llm_query_chunked`. Never treat the return as the answer.",
|
|
62
|
+
" Collect with `await_task(t)` or `await_task([t1,t2,…])`. Fire independent Tasks first, free work, then await.",
|
|
63
|
+
" Do NOT await after every independent spawn (serializes wall time). `task.done` when settled.",
|
|
64
|
+
"- `spawn(fn, *args) -> Task`: same as calling the always-spawn tools (not `llm_map_reduce`).",
|
|
65
|
+
"- Only `llm_map_reduce` still blocks until done.",
|
|
69
66
|
"",
|
|
70
67
|
" ```python",
|
|
71
|
-
" #
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
68
|
+
" # Multi-area study: one rlm_batch (parallel workers), free locate, then await",
|
|
69
|
+
" t = rlm_batch([",
|
|
70
|
+
" \"Study module A — NO edits. Paths + symbols for X.\",",
|
|
71
|
+
" \"Study module B — NO edits. Report how Y is configured.\",",
|
|
72
|
+
" ])",
|
|
73
|
+
" hits = search(\"X OR Y\", k=10)",
|
|
74
|
+
" reports = await_task(t)",
|
|
75
|
+
" # One-shot extracts: map_files / llm_batch also return Task → await_task",
|
|
75
76
|
" ```",
|
|
76
77
|
]);
|
|
77
78
|
|
|
@@ -89,7 +90,7 @@ export const RECURSION_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
|
89
90
|
" Narrow its world with `rlm_query(prompt, paths=['src/auth/', 'ctx/x-9f3a/'])` — path PREFIXES,",
|
|
90
91
|
" not globs. Omit `paths` to hand over everything.",
|
|
91
92
|
" Inheritance is one-way: sources the child loads, and its whole REPL, die with it — only its",
|
|
92
|
-
" final answer string returns.",
|
|
93
|
+
" final answer string returns. The child cannot write to your `answers` or `plan`.",
|
|
93
94
|
" At the depth cap `rlm_query` degrades to a plain sub-LLM call with NO context, which is why",
|
|
94
95
|
" this section disappears at the last recursive depth.",
|
|
95
96
|
]);
|
|
@@ -118,15 +119,15 @@ export const LARGE_FILE_RULE_LINES: readonly string[] = Object.freeze([
|
|
|
118
119
|
'1. Load in Python: `raw = open("dhat-heap.json").read()` — loading into a variable is fine.',
|
|
119
120
|
"2. Deterministic processing in Python (`json.load`, `re`, counting, aggregation) is fine and preferred.",
|
|
120
121
|
"3. The moment you need MEANING from raw text (summarize, explain, find anomalies), do NOT read it",
|
|
121
|
-
" yourself — call `llm_query_chunked(raw, question)
|
|
122
|
+
" yourself — call `llm_query_chunked(raw, question)` (Task → await_task), or slice + `llm_batch`.",
|
|
122
123
|
"4. Never print more than a small probe (~2K chars) of raw content.",
|
|
123
|
-
'Example: `
|
|
124
|
-
"aggregate `parts` in Python or with one final `llm_query
|
|
124
|
+
'Example: `t = llm_query_chunked(raw, "Extract top allocation sites with byte totals"); parts = await_task(t)`, then',
|
|
125
|
+
"aggregate `parts` in Python or with one final `llm_query` + await_task.",
|
|
125
126
|
]);
|
|
126
127
|
|
|
127
128
|
/** Concise native-mode glossary line for the chunked helper (native prompt has a 6K budget). */
|
|
128
129
|
export const CHUNKED_GLOSSARY_LINE_NATIVE =
|
|
129
|
-
"- `llm_query_chunked(text, prompt
|
|
130
|
+
"- `llm_query_chunked(text, prompt) -> Task` — always spawn; await_task → list[str] (one answer per chunk). Auto-splits oversized text.";
|
|
130
131
|
|
|
131
132
|
/** Concise native-mode large-file rule (folds in the context-exclusion note; native 6K budget). */
|
|
132
133
|
export const LARGE_FILE_RULE_NATIVE =
|
|
@@ -149,47 +150,45 @@ export const ENV_TIPS = [
|
|
|
149
150
|
"## Decomposition doctrine",
|
|
150
151
|
"",
|
|
151
152
|
"**Orchestrate; don't solve.** A single chain of thought over a large repository drifts —",
|
|
152
|
-
"you lose partials and compound mistakes.
|
|
153
|
-
"self-contained prompt and the text, they will extract, locate, classify, and summarize",
|
|
154
|
-
"reliably. Trust them; don't do their reading yourself.",
|
|
153
|
+
"you lose partials and compound mistakes. Sub-workers are competent: trust them; don't read for them.",
|
|
155
154
|
"",
|
|
156
|
-
"Your job: (1)
|
|
157
|
-
"(2)
|
|
158
|
-
"
|
|
159
|
-
"
|
|
155
|
+
"Your job: (1) free locate with `search` / `grep_context` / `outline`,",
|
|
156
|
+
"(2) fan out: **multi-step areas → `rlm_batch` / `rlm_query`**; one-shot extracts →",
|
|
157
|
+
" `map_files` / `llm_batch` (all return Task — `await_task` for content),",
|
|
158
|
+
"(3) memoize into `answers`, (4) sanity-check before dependents, (5) assemble from `answers`.",
|
|
160
159
|
"Your own compute is: pointers, dict lookups, string formatting, and decisions.",
|
|
161
160
|
"",
|
|
162
161
|
"### The only state that matters",
|
|
163
162
|
"`answers` and `plan` are dicts that persist across every turn.",
|
|
164
|
-
"**If a value isn't in `answers`, it doesn't exist.** Do not trust
|
|
165
|
-
"earlier reasoning or from truncated stdout — context drifts. Memoize everything you reuse.",
|
|
163
|
+
"**If a value isn't in `answers`, it doesn't exist.** Do not trust truncated stdout. Memoize.",
|
|
166
164
|
"",
|
|
167
165
|
"### Shape of a run",
|
|
168
|
-
"1. Probe: `print(len(context))`, `search(<
|
|
169
|
-
"2. Plan:
|
|
170
|
-
"3. Fan out
|
|
171
|
-
"
|
|
172
|
-
"4. Assemble
|
|
166
|
+
"1. Probe: `print(len(context))`, `search(<question>)`. Do not print file bodies.",
|
|
167
|
+
"2. Plan: sub-questions into `plan` (each from a named slice / module).",
|
|
168
|
+
"3. Fan out **in parallel**: one `rlm_batch` for independent multi-step studies, or",
|
|
169
|
+
" `map_files` / `llm_batch` for one-shot reads — not one serial call per file.",
|
|
170
|
+
"4. Assemble from `answers`.",
|
|
173
171
|
"",
|
|
174
172
|
"### Red flags — you are off track",
|
|
175
|
-
"- Printing file bodies
|
|
176
|
-
"-
|
|
177
|
-
"
|
|
178
|
-
"-
|
|
179
|
-
"-
|
|
180
|
-
"-
|
|
173
|
+
"- Printing file bodies / native bulk read → stop; use map_files or rlm_*.",
|
|
174
|
+
"- `llm_query(\"Read src/foo.ts…\")` with only a path — sub-LLM has **no disk**; use map_files/rlm_*.",
|
|
175
|
+
"- Multi-module task with zero `rlm_batch`/`rlm_query`/`map_files` → under-delegating.",
|
|
176
|
+
"- Await after every independent spawn → serializes wall time; fire-all-then-await.",
|
|
177
|
+
"- Treating Task as the answer without `await_task`.",
|
|
178
|
+
"- Regex used to *infer meaning* → sub-LLM job. Regex is for exact needles only.",
|
|
179
|
+
"- Two turns with zero sub-LLM calls on analysis → solving it yourself.",
|
|
181
180
|
].join("\n");
|
|
182
181
|
|
|
183
182
|
/** Native-mode variant of the doctrine — same rules, sized for the native prompt budget. */
|
|
184
183
|
export const ENV_TIPS_CONDENSED = [
|
|
185
|
-
"### Decomposition doctrine
|
|
186
|
-
"Orchestrate; don't solve.
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"Red flags:
|
|
191
|
-
"
|
|
192
|
-
"
|
|
184
|
+
"### Decomposition doctrine",
|
|
185
|
+
"Orchestrate; don't solve. Free locate → fan-out Tasks → await_task → memoize in `answers`.",
|
|
186
|
+
"Multi-module / multi-step areas: **`rlm_batch` (or rlm_query)** — not serial native read.",
|
|
187
|
+
"One-shot extracts: `map_files` / `llm_batch`. Always Task → await_task; fire-all then await.",
|
|
188
|
+
"`answers`/`plan` persist: **if it isn't in `answers`, it doesn't exist.**",
|
|
189
|
+
"Red flags: bulk file dumps; llm_query with path-only (no content — no disk!); zero rlm_*/map_files",
|
|
190
|
+
"on multi-area tasks; await after each spawn; Task treated as answer.",
|
|
191
|
+
"AUTHORING: you write every edit body yourself.",
|
|
193
192
|
].join("\n");
|
|
194
193
|
|
|
195
194
|
export function howToRunCode(): string {
|
|
@@ -224,21 +223,22 @@ export function replGlossary(
|
|
|
224
223
|
if (child) lines.push(...CHILD_CONTEXT_LINES);
|
|
225
224
|
lines.push(
|
|
226
225
|
"",
|
|
227
|
-
" Worked example — find the slice, then delegate it:",
|
|
226
|
+
" Worked example — find the slice, then delegate it (Task + await):",
|
|
228
227
|
" ```python",
|
|
229
228
|
' hits = search("where is the retry/backoff policy configured?", k=8)',
|
|
230
229
|
" paths = sorted({h['path'] for h in hits})",
|
|
231
|
-
'
|
|
230
|
+
' t = map_files(paths, "Describe any retry/backoff policy in this file, with line numbers. Say NONE if absent.")',
|
|
231
|
+
" answers.update(await_task(t))",
|
|
232
232
|
" print({p: a[:80] for p, a in answers.items()})",
|
|
233
233
|
" ```",
|
|
234
234
|
);
|
|
235
235
|
}
|
|
236
236
|
lines.push(...RETRIEVAL_GLOSSARY_LINES);
|
|
237
237
|
lines.push(
|
|
238
|
-
"- `llm_query(prompt: str
|
|
239
|
-
"
|
|
240
|
-
"- `
|
|
241
|
-
"
|
|
238
|
+
"- `llm_query(prompt: str) -> Task`: spawn one sub-LLM (await_task for str). The prompt must",
|
|
239
|
+
" **contain the text** to analyze — this call has no filesystem and no `context`.",
|
|
240
|
+
"- `llm_batch(prompts: list[str]) -> Task`: many parallel one-shots (same rule: embed text).",
|
|
241
|
+
" await_task → ordered list[str]. NEVER pass bare file paths as if the worker can open them.",
|
|
242
242
|
...CHUNKED_GLOSSARY_LINES,
|
|
243
243
|
...SPAWN_GLOSSARY_LINES,
|
|
244
244
|
...DELEGATION_GLOSSARY_LINES,
|
|
@@ -264,18 +264,14 @@ export function replGlossary(
|
|
|
264
264
|
}
|
|
265
265
|
if (recursion) {
|
|
266
266
|
lines.push(
|
|
267
|
-
"- `rlm_query(
|
|
268
|
-
"
|
|
269
|
-
" and never enters your history. Only the final answer (a short string) is returned.",
|
|
267
|
+
"- `rlm_query(task, paths=None) -> Task` / `rlm_batch(tasks, paths=None) -> Task`:",
|
|
268
|
+
" always spawn + ↯bg. await_task for the report string(s). Child REPL is private.",
|
|
270
269
|
"",
|
|
271
|
-
" **
|
|
272
|
-
" - `llm_query`
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
" execution (e.g. a sub-context large enough to need its own chunking, or a multi-step",
|
|
277
|
-
" reasoning chain). It is slower and more expensive — reserve it for cases `llm_query` cannot",
|
|
278
|
-
" handle. Avoid excessive recursive sub-calls when a batched one-shot would suffice.",
|
|
270
|
+
" **Routing (api_v5):**",
|
|
271
|
+
" - `llm_query` / `llm_batch` / `map_files` — one-shot facts/extracts (fast).",
|
|
272
|
+
" - `rlm_query` — one multi-step study (own search/outline loop).",
|
|
273
|
+
" - `rlm_batch` — ≥2 independent multi-step studies in **parallel** (prefer over N× rlm_query).",
|
|
274
|
+
" Always Task → await_task. Fire independent work first; never serial-await between peers.",
|
|
279
275
|
...RECURSION_CONTEXT_LINES,
|
|
280
276
|
);
|
|
281
277
|
}
|
|
@@ -285,6 +281,7 @@ export function replGlossary(
|
|
|
285
281
|
"- `SHOW_VARS() -> str`: list every variable currently in the REPL.",
|
|
286
282
|
'- `answer`: a dict initialized to {"content": "", "ready": False}. To submit your final answer,',
|
|
287
283
|
' set `answer["content"]` to the answer text and `answer["ready"] = True`.',
|
|
284
|
+
' **You MUST flip `answer["ready"] = True` — runs that never finalize are discarded.**',
|
|
288
285
|
);
|
|
289
286
|
return lines.join("\n");
|
|
290
287
|
}
|