@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.
Files changed (46) hide show
  1. package/README.md +131 -154
  2. package/README.ru.md +5 -5
  3. package/README.zh-CN.md +5 -5
  4. package/package.json +3 -2
  5. package/src/bridge/handlers/await.ts +148 -0
  6. package/src/bridge/handlers/completion.ts +72 -0
  7. package/src/bridge/handlers/emitting.ts +104 -0
  8. package/src/bridge/handlers/finish.ts +45 -0
  9. package/src/bridge/handlers/index.ts +48 -0
  10. package/src/bridge/handlers/llm-query.ts +130 -0
  11. package/src/bridge/handlers/rlm-query.ts +227 -0
  12. package/src/bridge/handlers/task-registry.ts +202 -0
  13. package/src/bridge/handlers/types.ts +136 -0
  14. package/src/commands/rlm-config.ts +40 -12
  15. package/src/config/settings.ts +13 -3
  16. package/src/context/listing.ts +2 -2
  17. package/src/context/refresh.ts +141 -0
  18. package/src/core/engine.ts +16 -18
  19. package/src/core/types.ts +1 -3
  20. package/src/index.ts +59 -54
  21. package/src/mode/native-guards.ts +4 -4
  22. package/src/mode/rlm-mode.ts +6 -1
  23. package/src/mode/subagent.ts +1 -1
  24. package/src/prompts/glossary.ts +71 -74
  25. package/src/prompts/native.ts +127 -85
  26. package/src/prompts/system.ts +29 -15
  27. package/src/sandbox/interrupts.ts +258 -68
  28. package/src/sandbox/protocol.ts +53 -30
  29. package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
  30. package/src/sandbox/py/__pycache__/hostio.cpython-314.pyc +0 -0
  31. package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
  32. package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
  33. package/src/sandbox/py/guards.py +8 -5
  34. package/src/sandbox/py/retrieval.py +17 -8
  35. package/src/sandbox/py/tasks.py +1 -1
  36. package/src/sandbox/py/worker.py +106 -79
  37. package/src/sandbox/sandbox-manager.ts +26 -1
  38. package/src/sandbox/sandbox.ts +1 -1
  39. package/src/tool/background-tasks.ts +1 -1
  40. package/src/tool/repl-result.ts +2 -2
  41. package/src/tool/repl-tool.ts +13 -14
  42. package/src/ui/config-panel.ts +1 -1
  43. package/src/ui/intro.ts +1 -4
  44. package/src/ui/model-picker.ts +32 -2
  45. package/src/util/concurrency.ts +1 -1
  46. 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 / llm_query_batched sub-call.
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, NATIVE_TURN_REMINDER } from "./prompts/native.ts";
23
- import { bashCommandFromInput, isFileReadingCommand, capToolResultText, BASH_BLOCK_REASON } from "./mode/native-guards.ts";
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
- const CAPPED_RESULT_TOOLS = Object.freeze(new Set(["bash", "find", "ls"]));
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 run a native tool flow; RLM's contract is the opposite
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
- // Wait for persisted settings before reading controller state
166
- await settingsReady;
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) controller.llmModel = 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
- && !(message.role === "user" && typeof message.content === "string" && message.content === NATIVE_TURN_REMINDER),
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
- "ANALYZE with repl({code}) read/grep are DISABLED.",
270
- "Files you have loaded live in the Python REPL `context` variable (starts empty; cwd seeds on first repl()).",
271
- "Locate with search()/grep_context()/outline() (free), then delegate bulk reading to",
272
- "map_files()/llm_query_batched(). Use add_context(path) for external dirs/files/docs/git URLs.",
273
- "If credits exhausted → report and stop.",
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
- // ── Native mode restrictions: keep bulk file content out of root-model context ──
294
- // `edit`/`write` stay unblocked so the agent modifies files through Pi's native
295
- // tool flow (visible to all plugins, +/- diff preview). File reading/searching
296
- // belongs in the REPL, and bash output is capped as a backstop.
297
- // Fail-open when repl is not active (e.g. --tools allowlist without repl): never
298
- // confiscate readers without a working substitute (RLM paper §2 trade).
299
- pi.on("tool_call", async (event) => {
300
- if (!nativeTradeHolds()) {
301
- if (traceEnabled && controller.enabled) {
302
- trace("native.block_skip", { toolName: event.toolName, reason: "no_active_repl" });
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 / llm_query_batched / " +
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 / llm_query_batched.]";
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
- "llm_query_batched / llm_query_chunked.]";
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 / llm_query_batched / llm_query_chunked. " +
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
  }
@@ -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
- llm: modelRef(this.llmModel) ?? this.savedLlmRef,
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
 
@@ -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 subcall-handlers.childRun; this module
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";
@@ -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. **Start here.** It is free:",
33
- " no sub-LLM call, no tokens. Use it before you guess at filenames or write regex.",
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: {path: n}, total, truncated} —",
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, model=None) -> dict[path, str]`: ask `prompt` of every file and",
45
- " get back {path: answer}. Accepts context entries or paths, packs them into cap-sized",
46
- " batched sub-calls, and splits oversized files automatically. **This is the default way to",
47
- " read many files** prefer it over hand-rolling a chunk loop.",
48
- "- `llm_map_reduce(items, map_prompt, reduce_prompt, model=None) -> str`: map over items in",
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, model=None) -> list[str]`: auto-splits `text` into",
56
- " chunks that fit the sub-LLM prompt cap, fans them out concurrently (order preserved), and",
57
- " returns one answer per chunk. Use it for ANY text too large for a single `llm_query` — a file",
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
- "- `spawn(fn, *args) -> Task`: start `llm_query`, `llm_query_batched`, `llm_query_chunked`,",
64
- " `map_files`, `rlm_query` or `rlm_query_batched` WITHOUT waiting. Returns immediately.",
65
- " (Not `llm_map_reduce` its reduce step depends on its own map results.)",
66
- "- `rlm_await(task)` / `rlm_await_all(tasks) -> list`: collect results; order matches input.",
67
- " Tasks survive across turns, so spawn the slow work first, keep doing useful things, and",
68
- " await only when you actually need the results. `task.done` tells you if it has landed.",
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
- " # start the slow sub-agents, then keep working while they run",
72
- " tasks = [spawn(rlm_query, f\"Audit {area} end to end\") for area in areas]",
73
- " hits = [f for f in context if \"TODO\" in f[\"content\"]] # overlaps with the sub-agents",
74
- " reports = rlm_await_all(tasks)",
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)`, or slice + `llm_query_batched`.",
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: `parts = llm_query_chunked(raw, "Extract top allocation sites with byte totals")`, then',
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, model=None) -> list[str]` — auto-splits oversized text into cap-sized chunks, fans out concurrently; one answer per chunk.";
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. Your sub-LLMs are competent readers: given a",
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) find the relevant slice with `search` / `grep_context` / `outline`,",
157
- "(2) delegate all semantic reading to `map_files` / `llm_query_batched` / `llm_map_reduce`,",
158
- "(3) memoize every result you will reuse in `answers`, (4) sanity-check an answer before",
159
- "another step depends on it, (5) assemble the final answer from `answers` by lookup.",
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 a number from your own",
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(<the user's question>)`. Do not print file bodies.",
169
- "2. Plan: write the sub-questions into `plan`; each must be answerable from a named slice.",
170
- "3. Fan out: one `map_files` / `llm_query_batched` per independent group, not one call per",
171
- " file. Store results into `answers` keyed by path or sub-question.",
172
- "4. Assemble: build the answer from `answers`. Delegate the aggregation too if it is large.",
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 to read them yourself → stop, delegate to `map_files`.",
176
- "- Writing regex to *infer meaning* (naming conventions, intent, correctness) that is a",
177
- " sub-LLM job. Regex is for exact lexical needles only.",
178
- "- Two turns in with zero sub-LLM calls on an analysis task you are solving it yourself.",
179
- "- About to reuse a value that is not in `answers` → re-derive it and store it.",
180
- "- One sub-call per file over dozens of files batch them; fat prompts in small batches win.",
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 spawnserializes 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 (paper App. C.3 — worth +69.5% there)",
186
- "Orchestrate; don't solve. Loop: `search`/`grep_context`/`outline` to find the slice →",
187
- "`map_files` / `llm_query_batched` to read it memoize into `answers` assemble by lookup.",
188
- "`answers` and `plan` persist across every turn: **if a value isn't in `answers`, it",
189
- "doesn't exist** never reuse a number from your own earlier reasoning or truncated stdout.",
190
- "Red flags: printing file bodies to read them; regex used to infer meaning rather than match",
191
- "a literal; two turns into an analysis with zero sub-LLM calls; one sub-call per file instead",
192
- "of one batch. Exception — AUTHORING is not reading: you write every edit body yourself.",
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
- ' answers.update(map_files(paths, "Describe any retry/backoff policy in this file, with line numbers. Say NONE if absent."))',
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, model=None) -> str`: a single sub-LLM completion. Use for extraction,",
239
- " summarization, or Q&A over a chunk of text.",
240
- "- `llm_query_batched(prompts: list[str], model=None) -> list[str]`: run several sub-LLM calls",
241
- " concurrently; output order matches input order.",
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(prompt, model=None)` / `rlm_query_batched(prompts, model=None)`: recursive RLM",
268
- " sub-calls. Each child runs a full REPL loop internally its entire conversation is PRIVATE",
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
- " **Choosing between `llm_query` and `rlm_query`:**",
272
- " - `llm_query` for simple one-shot taskssummarize a chunk, extract a fact, answer a direct",
273
- " question. It is a single LLM call: fast and cheap. Prefer it by default, and fan out with",
274
- " `llm_query_batched` for parallel one-shots.",
275
- " - `rlm_query` only when a sub-task genuinely needs iterative reasoning with its own code",
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
  }