@hicaru/pi-rlm 0.3.0 → 0.3.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 +52 -5
- package/README.ru.md +5 -5
- package/README.zh-CN.md +5 -5
- package/package.json +1 -1
- 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 +33 -14
- 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 +95 -38
- package/src/mode/native-guards.ts +4 -4
- package/src/mode/subagent.ts +68 -0
- 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 +15 -5
- package/src/sandbox/py/hostio.py +57 -0
- package/src/sandbox/py/retrieval.py +17 -8
- package/src/sandbox/py/tasks.py +1 -1
- package/src/sandbox/py/worker.py +109 -83
- package/src/sandbox/sandbox-manager.ts +26 -1
- package/src/sandbox/sandbox.ts +9 -2
- 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 +28 -2
- package/src/util/concurrency.ts +1 -1
- package/src/bridge/subcall-handlers.ts +0 -382
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
|
}
|
package/src/prompts/native.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/** Native-mode prompts — the main Pi agent drives the sandbox through the `repl` tool.
|
|
1
|
+
/** Native-mode prompts — the main Pi agent drives the sandbox through the `repl` tool.
|
|
2
|
+
*
|
|
3
|
+
* Structure mirrors rlm_test api_v5_anthropic (best bake-off arm): role → contract → routing →
|
|
4
|
+
* few-shots → anti-patterns → REPL surface. Goal: multi-area work fires rlm_batch / rlm_query
|
|
5
|
+
* as Task (BG), not serial repl+native read.
|
|
6
|
+
*/
|
|
2
7
|
|
|
3
8
|
import {
|
|
4
9
|
CHUNKED_GLOSSARY_LINE_NATIVE,
|
|
@@ -11,65 +16,52 @@ import {
|
|
|
11
16
|
/** Adapts the REPL glossary for native mode — agent calls `repl({code})` instead of writing ```repl``` blocks. */
|
|
12
17
|
function nativeReplGlossary(): string {
|
|
13
18
|
return [
|
|
14
|
-
"##
|
|
15
|
-
"",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"",
|
|
19
|
-
"
|
|
20
|
-
"- `
|
|
21
|
-
"",
|
|
22
|
-
"
|
|
23
|
-
"-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
19
|
+
"## REPL surface (inside `repl({code})` only)",
|
|
20
|
+
"",
|
|
21
|
+
"Persistent Python sandbox. Only `print(...)` returns; variables/`answers`/`plan` persist across repl calls.",
|
|
22
|
+
"",
|
|
23
|
+
"### Free locate (no sub-LLM tokens)",
|
|
24
|
+
"- `search(query, k=10, path_glob=None) -> [{path, line, score, snippet, text}]` — BM25 pointers, not bodies",
|
|
25
|
+
"- `grep_context(pattern, k=50, …) -> {hits, counts, total, truncated}` — regex / lexical needles",
|
|
26
|
+
"- `outline(path) -> str` — definition skeleton (~200 chars)",
|
|
27
|
+
"",
|
|
28
|
+
"### Always-spawn fan-out (return Task + run ↯bg — NEVER the answer)",
|
|
29
|
+
"| Call | await_task → | When | NOT for |",
|
|
30
|
+
"|------|--------------|------|---------|",
|
|
31
|
+
"| `llm_query(prompt)` | str | cheap fact **from text in prompt** | reading files (no disk) |",
|
|
32
|
+
"| `llm_batch(prompts)` | list[str] | many cheap facts in **parallel** | multi-step research |",
|
|
33
|
+
"| `map_files(files, prompt)` | dict[path,str] | same question over many files | multi-step per file |",
|
|
34
|
+
"| `llm_query_chunked(text, prompt)` | list[str] | one huge text, auto-chunked | structured data |",
|
|
35
|
+
"| `rlm_query(task, paths=None)` | str | **one** multi-step study | ≥2 independent studies |",
|
|
36
|
+
"| `rlm_batch(tasks, paths=None)` | list[str] | **≥2 independent studies in parallel** | trivia / one-shots |",
|
|
37
|
+
"",
|
|
38
|
+
"Collect: `await_task(t)` or `await_task([t1,t2,…])`. `task.done` when settled.",
|
|
39
|
+
"The child inherits your `context`; send instructions + optional `paths=['src/auth/']` prefixes — never paste file bodies. **Children are sandboxed — they cannot mutate your `answers`, `plan`, or REPL variables.**",
|
|
32
40
|
CHUNKED_GLOSSARY_LINE_NATIVE,
|
|
33
|
-
"- `
|
|
34
|
-
"- `
|
|
35
|
-
"- `spawn(fn, *args) -> Task` / `rlm_await(t)` / `rlm_await_all(ts)` — start `llm_query`, `llm_query_batched`, `llm_query_chunked`, `map_files`, `rlm_query` or `rlm_query_batched` without waiting (NOT `llm_map_reduce`); collect later, order preserved. Tasks outlive the repl() call, so spawn slow work early and await when you need it.",
|
|
36
|
-
"",
|
|
41
|
+
"- `llm_map_reduce(...)` **blocks** (map then reduce) — prefer Tasks when you can interleave free work.",
|
|
42
|
+
"- `spawn(fn, *args) -> Task` optional alias for always-spawn tools (not llm_map_reduce).",
|
|
37
43
|
"",
|
|
38
|
-
"
|
|
39
|
-
"- `
|
|
40
|
-
"- `
|
|
41
|
-
"- `
|
|
44
|
+
"### Memo / finalize",
|
|
45
|
+
"- `answers` / `plan` — persistent dicts. **If a value isn't in `answers`, it doesn't exist.**",
|
|
46
|
+
"- `add_context(source) -> dict` — append external dir/file/doc/git under `ctx/<id>/…` (metadata only).",
|
|
47
|
+
"- `SHOW_VARS()` — list REPL vars. `answer[\"ready\"]=True` only for headless finalize (native: write a normal message).",
|
|
42
48
|
"",
|
|
43
49
|
ENV_TIPS_CONDENSED,
|
|
44
50
|
"",
|
|
45
|
-
|
|
46
|
-
"```python",
|
|
47
|
-
'hits = search("where is retry/backoff configured?", k=8)',
|
|
48
|
-
"paths = sorted({h['path'] for h in hits})",
|
|
49
|
-
'answers.update(map_files(paths, "Describe any retry/backoff policy here, with line numbers. Say NONE if absent."))',
|
|
50
|
-
"print({p: a[:80] for p, a in answers.items()})",
|
|
51
|
-
"```",
|
|
52
|
-
`- Sub-prompts cap at ${DEFAULT_PROMPT_CAP.toLocaleString()} chars (≈${promptCapTokensK(DEFAULT_PROMPT_CAP)}K tokens); ~20 prompts per batch. Fat prompts in small batches > thousands of tiny prompts.`,
|
|
51
|
+
`- Sub-prompts cap ${DEFAULT_PROMPT_CAP.toLocaleString()} chars (≈${promptCapTokensK(DEFAULT_PROMPT_CAP)}K tok); ~20 prompts/batch.`,
|
|
53
52
|
"",
|
|
54
|
-
"###
|
|
53
|
+
"### Outside REPL",
|
|
55
54
|
"| Tool | When |",
|
|
56
55
|
"|------|------|",
|
|
57
|
-
"| `repl({code})` |
|
|
58
|
-
"| `edit` / `write` |
|
|
59
|
-
"| `
|
|
60
|
-
"| `
|
|
61
|
-
"| `map_files` / `llm_query_batched` (in repl) | Read/extract/classify that slice |",
|
|
62
|
-
"| `rlm_query` (in repl) | Sub-task needing its own iterative reasoning and REPL |",
|
|
63
|
-
"",
|
|
64
|
-
"### Task-Specific Patterns",
|
|
65
|
-
LARGE_FILE_RULE_NATIVE,
|
|
66
|
-
"- Architecture/code review: `search` for the subsystem, then `map_files` the hits.",
|
|
67
|
-
"- Bug investigation: `grep_context` for the literal symbol/message, then `map_files` the matching files.",
|
|
68
|
-
"- Finalizing: for file changes call `edit`/`write` directly so Pi validates the anchor and renders the diff; for analysis, write a normal message.",
|
|
69
|
-
"- If sub-LLM credits are exhausted, report partial results and stop — do not bypass REPL restrictions.",
|
|
56
|
+
"| `repl({code})` | **All** bulk repo locate + fan-out analysis |",
|
|
57
|
+
"| `edit` / `write` | Apply a change **you** authored after research |",
|
|
58
|
+
"| `zebra-mcp` | Embedding search when lexical `search` misses |",
|
|
59
|
+
"| native `read`/`grep` | Only tiny pin-point files for an **edit anchor** — never bulk study |",
|
|
70
60
|
"",
|
|
71
|
-
|
|
72
|
-
"
|
|
61
|
+
LARGE_FILE_RULE_NATIVE,
|
|
62
|
+
"- Architecture/multi-module: `rlm_batch` per area (or map_files for one-shot extracts).",
|
|
63
|
+
"- Bug hunt: `grep_context` needle → `map_files` / `rlm_query` on hits — not serial native read.",
|
|
64
|
+
"- Credits exhausted → report partials and stop.",
|
|
73
65
|
].join("\n");
|
|
74
66
|
}
|
|
75
67
|
|
|
@@ -80,48 +72,98 @@ export function buildNativeSystemPrompt(): string {
|
|
|
80
72
|
"║ NATIVE RLM MODE — YOU ARE AN ORCHESTRATOR, NOT A READER ║",
|
|
81
73
|
"╚══════════════════════════════════════════════════════════════════╝",
|
|
82
74
|
"",
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
75
|
+
"You never invent file contents. Multi-step / multi-area repo work goes through the REPL fan-out API.",
|
|
76
|
+
"Large tool/repl outputs are hard-capped (~4K chars).",
|
|
77
|
+
"",
|
|
78
|
+
"AUTHORING RULE: sub-LLMs (`llm_query` / `rlm_query` family) READ only — extract, locate, summarize.",
|
|
79
|
+
"They never author code you will ship. Once you know WHAT to change, compose exact oldText/newText",
|
|
80
|
+
"yourself and apply with `edit` / `write`. Never re-type file bodies or Task results into chat.",
|
|
81
|
+
"",
|
|
82
|
+
"THINKING RULE: complex decomposition (≥3 modules, architecture decisions, uncertainty",
|
|
83
|
+
"about which files hold the answer) → think out loud before the first repl() call.",
|
|
84
|
+
"Simple lookups, known paths, one-shot extracts → jump straight to repl().",
|
|
85
|
+
"Premature repl() calls on uncertain targets waste more tokens than 1–2 sentences of planning.",
|
|
86
|
+
"",
|
|
87
|
+
"<contract>",
|
|
88
|
+
"Inside `repl({code})`, EVERY heavy call returns a Task immediately (not the answer):",
|
|
89
|
+
" llm_query | llm_batch | map_files | llm_query_chunked | rlm_query | rlm_batch → Task",
|
|
90
|
+
"ONLY `await_task(t)` / `await_task([…])` returns content. Fan-out runs detached (↯bg) and outlives the cell.",
|
|
91
|
+
"If you printed a Task and did not await_task, you do **not** know the answer yet.",
|
|
92
|
+
"Fire independent Tasks first → free `search`/`grep_context`/`outline` → then await_task.",
|
|
93
|
+
"Do NOT await after every independent spawn (that serializes wall time).",
|
|
94
|
+
"</contract>",
|
|
95
|
+
"",
|
|
96
|
+
"<routing>",
|
|
97
|
+
"Pick the smallest tool that fits:",
|
|
98
|
+
"- One cheap pure-text fact **with the text already in the prompt** → llm_query",
|
|
99
|
+
"- Many similar facts/chunks **with text embedded** → llm_batch(prompts=[…])",
|
|
100
|
+
"- Same question over repo files in `context` → **map_files(files|paths, prompt)** // attaches content",
|
|
101
|
+
"- One multi-step study (own locate loop over context) → rlm_query(task=…, paths=…)",
|
|
102
|
+
"- ≥2 independent multi-step areas/modules → **rlm_batch(tasks=[…])** // PREFER over serial rlm_query",
|
|
103
|
+
"- Dependent edit: rlm_query(research NO edits) → await_task → then YOU write edit/write",
|
|
104
|
+
"CRITICAL: `llm_query` / `llm_batch` have **NO filesystem and NO `context`** — only the string you pass.",
|
|
105
|
+
"NEVER: llm_query(\"Read pi-plugin/…/foo.ts fully…\") — the worker will say it cannot access files.",
|
|
106
|
+
"To study real files: map_files / rlm_query / rlm_batch (they see `context`) OR put the slice in the prompt yourself.",
|
|
107
|
+
"NEVER use llm_batch for \"fix the whole codebase\". NEVER use rlm_batch for trivia.",
|
|
108
|
+
"NEVER bulk-read the repo with native read/grep or by printing file bodies in repl.",
|
|
109
|
+
"</routing>",
|
|
110
|
+
"",
|
|
111
|
+
"<examples>",
|
|
112
|
+
"E1 multi-area study (PREFER rlm_batch — parallel workers):",
|
|
113
|
+
"```python",
|
|
114
|
+
"t = rlm_batch([",
|
|
115
|
+
' "Study pi-plugin/rlm/src/commands/ — how is RLM toggled ON/OFF? NO edits. Paths + symbols.",',
|
|
116
|
+
' "Study pi-plugin/rlm/src/mode/rlm-mode.ts + settings — enabled default + persistence. NO edits.",',
|
|
117
|
+
' "Study pi-plugin/rlm/src/ui/status.ts — status line format. NO edits. Exact string patterns.",',
|
|
118
|
+
"])",
|
|
119
|
+
"hits = search(\"setEnabled OR toggle OR enabled\", k=12) # free while workers run",
|
|
120
|
+
"print([h[\"path\"] for h in hits[:8]])",
|
|
121
|
+
"reports = await_task(t)",
|
|
122
|
+
"answers[\"toggle\"] = reports",
|
|
123
|
+
"print({i: r[:200] for i, r in enumerate(reports)})",
|
|
124
|
+
"```",
|
|
86
125
|
"",
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
"
|
|
126
|
+
"E2 locate then map_files (one-shot extracts, not multi-step):",
|
|
127
|
+
"```python",
|
|
128
|
+
'hits = search("where is retry/backoff configured?", k=8)',
|
|
129
|
+
"paths = sorted({h['path'] for h in hits})",
|
|
130
|
+
't = map_files(paths, "Describe any retry/backoff policy; line numbers. NONE if absent.")',
|
|
131
|
+
"answers.update(await_task(t))",
|
|
132
|
+
"print({p: a[:80] for p, a in answers.items()})",
|
|
133
|
+
"```",
|
|
92
134
|
"",
|
|
93
|
-
"
|
|
94
|
-
"
|
|
135
|
+
"E3 parallel llm_batch:",
|
|
136
|
+
"```python",
|
|
137
|
+
't = llm_batch(["one-line purpose of file A", "one-line purpose of file B"])',
|
|
138
|
+
"print(await_task(t))",
|
|
139
|
+
"```",
|
|
95
140
|
"",
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
141
|
+
"E4 BAD: serial native read of many files, or await after every independent spawn",
|
|
142
|
+
" → Wall-clock time scales linearly. Fire independent Tasks first, then await all.",
|
|
143
|
+
"E5 BAD: treat Task / map_files return as the answer without await_task",
|
|
144
|
+
" → You'll read a Task repr, not the data. Silent garbage.",
|
|
145
|
+
"E6 BAD: one giant rlm_query that tries study+edit+verify for the whole tree",
|
|
146
|
+
" → Token limits + no parallelism. Split into rlm_batch tasks per module.",
|
|
147
|
+
"E7 BAD: llm_batch([\"Read path/to/a.ts…\", \"Read path/to/b.ts…\"]) — paths only, no content → useless",
|
|
148
|
+
" → llm_batch has no filesystem: it only sees the text string you paste.",
|
|
149
|
+
"E7 GOOD instead: map_files([\"path/to/a.ts\",\"path/to/b.ts\"], \"…question…\") or rlm_batch multi-step tasks",
|
|
150
|
+
"</examples>",
|
|
151
|
+
"",
|
|
152
|
+
"<rules>",
|
|
153
|
+
"1. LOCATE-THEN-DELEGATE: free search/grep/outline first; semantic reading only via Task tools.",
|
|
154
|
+
"2. Multi-module / multi-question tasks → rlm_batch (or several Tasks) in **one** repl when independent.",
|
|
155
|
+
"3. Self-contained task strings: goal + scope + success + \"NO edits\" for pure study.",
|
|
156
|
+
"4. AUTHORING: sub-LLMs READ only. You compose every edit oldText/newText yourself.",
|
|
157
|
+
"5. Memoize into `answers`. Never reuse facts only from truncated stdout.",
|
|
158
|
+
"6. Cap ~4–6 heavy concurrent rlm workers; prefer one rlm_batch over N serial rlm_query.",
|
|
159
|
+
"</rules>",
|
|
101
160
|
"",
|
|
102
161
|
nativeReplGlossary(),
|
|
103
162
|
].join("\n");
|
|
104
163
|
}
|
|
105
164
|
|
|
106
|
-
/** Soft cap on the static native prompt.
|
|
107
|
-
|
|
108
|
-
* Raised from 6K when the retrieval glossary and the condensed decomposition doctrine
|
|
109
|
-
* landed; both buy far more than they cost (paper Table 2, Fig. 4a), then again for
|
|
110
|
-
* spawn/rlm_await: the async fan-out API is part of the model-visible contract, and
|
|
111
|
-
* ~50 tokens is worth the model actually using it. */
|
|
112
|
-
export const NATIVE_PROMPT_BUDGET = 7_700;
|
|
165
|
+
/** Soft cap on the static native prompt. Raised for v5-style contract/routing/examples. */
|
|
166
|
+
export const NATIVE_PROMPT_BUDGET = 9_500;
|
|
113
167
|
|
|
114
168
|
/** Exported for tests — prompt length without context metadata (which is injected separately). */
|
|
115
169
|
export const NATIVE_PROMPT_STATIC = buildNativeSystemPrompt();
|
|
116
|
-
|
|
117
|
-
/** Per-turn last-position reminder for native mode — appended to every context build. */
|
|
118
|
-
export const NATIVE_TURN_REMINDER = [
|
|
119
|
-
"[RLM orchestrator contract — enforced by the runtime, not optional:",
|
|
120
|
-
"repl() stdout to you is hard-capped at 4K chars; read/grep and bash readers are blocked.",
|
|
121
|
-
"LOCATE FIRST with search() / grep_context() / outline() — they cost nothing. Any SEMANTIC",
|
|
122
|
-
"reading MUST then go through map_files / llm_query / llm_query_batched / llm_query_chunked",
|
|
123
|
-
"(rlm_query for iterative sub-tasks). Memoize what you reuse in `answers`; a value not in",
|
|
124
|
-
"`answers` does not exist. AUTHORING IS NOT READING: you write every edit body yourself and",
|
|
125
|
-
"apply it with the native `edit` / `write` tools — never delegate code you will ship.",
|
|
126
|
-
"Keep your own output to decisions, authored edits, and aggregation.]",
|
|
127
|
-
].join("\n");
|
package/src/prompts/system.ts
CHANGED
|
@@ -31,26 +31,31 @@ export interface SystemPromptOptions {
|
|
|
31
31
|
readonly contextLoader?: boolean;
|
|
32
32
|
/** depth > 0 — this run is an rlm_query child and its `context` is the parent's world. */
|
|
33
33
|
readonly child?: boolean;
|
|
34
|
+
/** The recursion depth of this run (0 = root, 1 = first child, etc.). */
|
|
35
|
+
readonly depth?: number;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
function orchestratorAddendum(maxPromptChars: number): string {
|
|
37
39
|
return [
|
|
38
|
-
"As an RLM you are an **orchestrator, not a solver**.
|
|
39
|
-
"
|
|
40
|
-
"at a time, printing a small sample of each result to verify before moving on.",
|
|
40
|
+
"As an RLM you are an **orchestrator, not a solver**. Probe `context`, plan decomposition, then",
|
|
41
|
+
"fan out — do not solve multi-step module work yourself in a long chain of thought.",
|
|
41
42
|
"",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"already pin the answer, just read it directly. Aggregate the small results back in Python.",
|
|
43
|
+
"<contract> llm_query / llm_batch / map_files / rlm_query / rlm_batch return Task (not the answer).",
|
|
44
|
+
"Only await_task returns content. Fire independent Tasks first, free search, then await_task.",
|
|
45
|
+
"Do not await after every independent spawn.</contract>",
|
|
46
46
|
"",
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"If the workload exceeds both at once, filter aggressively in Python first, then batch the survivors.",
|
|
47
|
+
"<routing> one-shot facts → llm_query/llm_batch/map_files; one multi-step study → rlm_query;",
|
|
48
|
+
"≥2 independent multi-step areas → rlm_batch (prefer over serial rlm_query). NEVER print file",
|
|
49
|
+
"bodies into your own stream when a Task tool can read them.</routing>",
|
|
51
50
|
"",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
51
|
+
"Your own context window is small. Push long-context work into sub-calls. If free search/grep",
|
|
52
|
+
"already pins a tiny fact, use that. Aggregate small results in Python / `answers`.",
|
|
53
|
+
"",
|
|
54
|
+
`Sub-call budget: (1) per-prompt < ${maxPromptChars.toLocaleString()} chars (≈${promptCapTokensK(maxPromptChars)}K tok);`,
|
|
55
|
+
"(2) ~20 prompts per llm_batch. Fat prompts in small batches beat thousands of tiny prompts.",
|
|
56
|
+
"Filter in Python first when both axes overflow.",
|
|
57
|
+
"",
|
|
58
|
+
"Reserve your tokens for planning, combining, and finalizing. Do not finalize before inspecting `context`.",
|
|
54
59
|
].join("\n");
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -66,6 +71,15 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
66
71
|
const maxPromptChars = opts.maxPromptChars ?? DEFAULT_PROMPT_CAP;
|
|
67
72
|
const parts = [
|
|
68
73
|
INTRO,
|
|
74
|
+
];
|
|
75
|
+
if ((opts.depth ?? 0) > 0) {
|
|
76
|
+
parts.push(
|
|
77
|
+
"",
|
|
78
|
+
`**Recursion depth: ${opts.depth}.** You are a sub-RLM — focus narrowly on your assigned`,
|
|
79
|
+
"task. Delegate (rlm_query/rlm_batch) only if the task itself must decompose further.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
parts.push(
|
|
69
83
|
"",
|
|
70
84
|
howToRunCode(),
|
|
71
85
|
"",
|
|
@@ -78,7 +92,7 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
78
92
|
"dump a whole sub-LLM result. The full content persists across turns in REPL variables (call `SHOW_VARS()`).",
|
|
79
93
|
"",
|
|
80
94
|
"Start by probing `context` (print a few lines, count items). Then build up an answer to the query.",
|
|
81
|
-
|
|
95
|
+
);
|
|
82
96
|
if (opts.orchestrator ?? true) {
|
|
83
97
|
// Two counterweights, both required (paper App. B): the addendum bounds OVER-recursion
|
|
84
98
|
// (batching/cost), ENV_TIPS bounds UNDER-recursion (solving it yourself).
|
|
@@ -102,5 +116,5 @@ export function buildMetadataLine(meta: PromptMeta, maxPromptChars = DEFAULT_PRO
|
|
|
102
116
|
? ` Your context has ${meta.contextStats.files} files; per-file tokens run min ${meta.contextStats.min.toLocaleString()} / median ${meta.contextStats.median.toLocaleString()} / max ${meta.contextStats.max.toLocaleString()} — use this to gauge how many files fit per batch.`
|
|
103
117
|
: "";
|
|
104
118
|
const body = `${contextDesc} ${tail}${dist}`;
|
|
105
|
-
return meta.rootPrompt ?
|
|
119
|
+
return meta.rootPrompt ? `<task>${meta.rootPrompt}</task>\n\n${body}` : body;
|
|
106
120
|
}
|