@hicaru/pi-rlm 0.1.9 → 0.2.1
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/package.json +1 -1
- package/src/bridge/fallback-todo.ts +12 -1
- package/src/bridge/subcall-handlers.ts +336 -0
- package/src/commands/rlm-config.ts +8 -8
- package/src/commands/rlm.ts +48 -12
- package/src/config/defaults.ts +4 -1
- package/src/config/settings.ts +33 -3
- package/src/context/repomix-context.ts +5 -10
- package/src/core/answer.ts +4 -3
- package/src/core/artifacts.ts +4 -3
- package/src/core/engine.ts +101 -267
- package/src/core/gates.ts +3 -3
- package/src/core/limits.ts +19 -1
- package/src/core/pipeline-handlers.ts +319 -0
- package/src/core/pipeline.ts +2 -2
- package/src/core/types.ts +25 -27
- package/src/index.ts +63 -17
- package/src/mode/rlm-mode.ts +8 -11
- package/src/prompts/system.ts +164 -52
- package/src/prompts/user.ts +1 -5
- package/src/sandbox/protocol.ts +6 -7
- package/src/sandbox/sandbox-manager.ts +25 -11
- package/src/sandbox/sandbox.ts +93 -22
- package/src/sandbox/worker.py +798 -66
- package/src/state/paths.ts +1 -1
- package/src/state/reads.ts +12 -4
- package/src/state/resume.ts +5 -11
- package/src/text/parsing.ts +0 -6
- package/src/tool/background-tasks.ts +95 -0
- package/src/tool/repl-details.ts +2 -0
- package/src/tool/repl-tool.ts +223 -318
- package/src/tool/rlm-details.ts +0 -10
- package/src/tool/rlm-events.ts +10 -2
- package/src/tool/rlm-tool.ts +18 -31
- package/src/tool/subcall-render.ts +75 -11
- package/src/tool/subcall-store.ts +57 -1
- package/src/ui/config-panel.ts +41 -21
- package/src/ui/intro.ts +2 -1
- package/src/ui/status.ts +8 -5
- package/src/ui/theme-adapter.ts +36 -0
- package/src/ui/theme.ts +0 -25
- package/src/util/concurrency.ts +87 -13
- package/src/util/trace.ts +42 -0
- package/src/bridge/llm-query.ts +0 -133
- package/src/bridge/rlm-query.ts +0 -122
- package/src/mode/input-router.ts +0 -23
package/src/prompts/system.ts
CHANGED
|
@@ -36,6 +36,36 @@ function promptCapTokensK(maxPromptChars: number): number {
|
|
|
36
36
|
return Math.round(maxPromptChars / 4_000);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Deterministic retrieval over `context` (headless + native).
|
|
41
|
+
*
|
|
42
|
+
* The paper's trajectories retrieve with hand-written regex (App. E.1); frontier models do that
|
|
43
|
+
* well, small ones guess keywords badly, and the first decomposition disproportionately decides
|
|
44
|
+
* the outcome (§5, Fig. 4a). These cost no tokens and no sub-calls.
|
|
45
|
+
*/
|
|
46
|
+
const RETRIEVAL_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
47
|
+
"- `search(query: str, k=10, path_glob=None)`: BM25 ranking over `context`. Returns",
|
|
48
|
+
" [{path, line, score, snippet}] — POINTERS, not bodies. **Start here.** It is free:",
|
|
49
|
+
" no sub-LLM call, no tokens. Use it before you guess at filenames or write regex.",
|
|
50
|
+
"- `grep_context(pattern, k=50, path_glob=None, before=0, after=0) -> dict`: regex over",
|
|
51
|
+
" `context`. Returns {hits: [{path, line, text}], counts: {path: n}, total, truncated} —",
|
|
52
|
+
" `counts` is complete even when `hits` is capped, so a wide pattern reports its shape",
|
|
53
|
+
" instead of flooding you. Use for exact lexical needles; use `search` for meaning.",
|
|
54
|
+
"- `outline(path) -> str`: definition/heading skeleton of one file with line numbers.",
|
|
55
|
+
" Orient in ~200 chars instead of printing 20K. Matches exact path, then suffix, then glob.",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/** One-line delegation helpers — orchestrating must be cheaper than solving. */
|
|
59
|
+
const DELEGATION_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
60
|
+
"- `map_files(files, prompt, model=None) -> dict[path, str]`: ask `prompt` of every file and",
|
|
61
|
+
" get back {path: answer}. Accepts context entries or paths, packs them into cap-sized",
|
|
62
|
+
" batched sub-calls, and splits oversized files automatically. **This is the default way to",
|
|
63
|
+
" read many files** — prefer it over hand-rolling a chunk loop.",
|
|
64
|
+
"- `llm_map_reduce(items, map_prompt, reduce_prompt, model=None) -> str`: map over items in",
|
|
65
|
+
" one batch, then reduce the partial answers with a single call. The paper's canonical",
|
|
66
|
+
" strategy (query per chunk → aggregate the buffers) as one call.",
|
|
67
|
+
]);
|
|
68
|
+
|
|
39
69
|
/** Shared glossary entry for the chunked-query helper (headless + native). */
|
|
40
70
|
const CHUNKED_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
41
71
|
"- `llm_query_chunked(text: str, prompt: str, model=None) -> list[str]`: auto-splits `text` into",
|
|
@@ -44,6 +74,23 @@ const CHUNKED_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
44
74
|
" you open()ed, an oversized sub-result, or several concatenated context files.",
|
|
45
75
|
]);
|
|
46
76
|
|
|
77
|
+
/** Non-blocking fan-out: spawn now, collect later (headless glossary). */
|
|
78
|
+
const SPAWN_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
79
|
+
"- `spawn(fn, *args) -> Task`: start `llm_query`, `llm_query_batched`, `llm_query_chunked`,",
|
|
80
|
+
" `map_files`, `rlm_query` or `rlm_query_batched` WITHOUT waiting. Returns immediately.",
|
|
81
|
+
" (Not `llm_map_reduce` — its reduce step depends on its own map results.)",
|
|
82
|
+
"- `rlm_await(task)` / `rlm_await_all(tasks) -> list`: collect results; order matches input.",
|
|
83
|
+
" Tasks survive across turns, so spawn the slow work first, keep doing useful things, and",
|
|
84
|
+
" await only when you actually need the results. `task.done` tells you if it has landed.",
|
|
85
|
+
"",
|
|
86
|
+
" ```python",
|
|
87
|
+
" # start the slow sub-agents, then keep working while they run",
|
|
88
|
+
" tasks = [spawn(rlm_query, f\"Audit {area} end to end\") for area in areas]",
|
|
89
|
+
" hits = [f for f in context if \"TODO\" in f[\"content\"]] # overlaps with the sub-agents",
|
|
90
|
+
" reports = rlm_await_all(tasks)",
|
|
91
|
+
" ```",
|
|
92
|
+
]);
|
|
93
|
+
|
|
47
94
|
/** Why a file the user mentioned may be missing from `context`. */
|
|
48
95
|
const CONTEXT_EXCLUSION_NOTE =
|
|
49
96
|
" NOTE: files larger than 1MB and gitignored files are NOT in `context` — they exist only on disk.";
|
|
@@ -69,6 +116,66 @@ const CHUNKED_GLOSSARY_LINE_NATIVE =
|
|
|
69
116
|
const LARGE_FILE_RULE_NATIVE =
|
|
70
117
|
"- Files >1MB or gitignored are NOT in `context`: open() + parse deterministically in Python is fine; ANY semantic reading of the raw text goes through llm_query_chunked. Never print >2K chars raw.";
|
|
71
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The decomposition doctrine, ported from the RLM paper's Appendix C.3 `<env_tips>` and
|
|
121
|
+
* retargeted from competition math to repository analysis.
|
|
122
|
+
*
|
|
123
|
+
* This block is the single highest-leverage prompt intervention the paper reports: +69.5% on
|
|
124
|
+
* LongCoT-mini over the same RLM without it (Table 2). Plain RLM prompting alone actually
|
|
125
|
+
* *regressed* two of the five categories; the doctrine is what fixed them. Its purpose is to
|
|
126
|
+
* counter under-delegation — the model doing the work itself in the REPL instead of fanning out.
|
|
127
|
+
*
|
|
128
|
+
* Note the counterweight: `orchestratorAddendum` carries the anti-OVER-recursion batching rule.
|
|
129
|
+
* The paper is explicit (App. B) that one prompt does not port across models and that both
|
|
130
|
+
* guardrails are needed; keep them both.
|
|
131
|
+
*/
|
|
132
|
+
const ENV_TIPS = [
|
|
133
|
+
"## Decomposition doctrine",
|
|
134
|
+
"",
|
|
135
|
+
"**Orchestrate; don't solve.** A single chain of thought over a large repository drifts —",
|
|
136
|
+
"you lose partials and compound mistakes. Your sub-LLMs are competent readers: given a",
|
|
137
|
+
"self-contained prompt and the text, they will extract, locate, classify, and summarize",
|
|
138
|
+
"reliably. Trust them; don't do their reading yourself.",
|
|
139
|
+
"",
|
|
140
|
+
"Your job: (1) find the relevant slice with `search` / `grep_context` / `outline`,",
|
|
141
|
+
"(2) delegate all semantic reading to `map_files` / `llm_query_batched` / `llm_map_reduce`,",
|
|
142
|
+
"(3) memoize every result you will reuse in `answers`, (4) sanity-check an answer before",
|
|
143
|
+
"another step depends on it, (5) assemble the final answer from `answers` by lookup.",
|
|
144
|
+
"Your own compute is: pointers, dict lookups, string formatting, and decisions.",
|
|
145
|
+
"",
|
|
146
|
+
"### The only state that matters",
|
|
147
|
+
"`answers` and `plan` are dicts that persist across every turn and survive snapshots.",
|
|
148
|
+
"**If a value isn't in `answers`, it doesn't exist.** Do not trust a number from your own",
|
|
149
|
+
"earlier reasoning or from truncated stdout — context drifts. Memoize everything you reuse.",
|
|
150
|
+
"",
|
|
151
|
+
"### Shape of a run",
|
|
152
|
+
"1. Probe: `print(len(context))`, `search(<the user's question>)`. Do not print file bodies.",
|
|
153
|
+
"2. Plan: write the sub-questions into `plan`; each must be answerable from a named slice.",
|
|
154
|
+
"3. Fan out: one `map_files` / `llm_query_batched` per independent group, not one call per",
|
|
155
|
+
" file. Store results into `answers` keyed by path or sub-question.",
|
|
156
|
+
"4. Assemble: build the answer from `answers`. Delegate the aggregation too if it is large.",
|
|
157
|
+
"",
|
|
158
|
+
"### Red flags — you are off track",
|
|
159
|
+
"- Printing file bodies to read them yourself → stop, delegate to `map_files`.",
|
|
160
|
+
"- Writing regex to *infer meaning* (naming conventions, intent, correctness) → that is a",
|
|
161
|
+
" sub-LLM job. Regex is for exact lexical needles only.",
|
|
162
|
+
"- Two turns in with zero sub-LLM calls on an analysis task → you are solving it yourself.",
|
|
163
|
+
"- About to reuse a value that is not in `answers` → re-derive it and store it.",
|
|
164
|
+
"- One sub-call per file over dozens of files → batch them; fat prompts in small batches win.",
|
|
165
|
+
].join("\n");
|
|
166
|
+
|
|
167
|
+
/** Native-mode variant of the doctrine — same rules, sized for the native prompt budget. */
|
|
168
|
+
const ENV_TIPS_CONDENSED = [
|
|
169
|
+
"### Decomposition doctrine (paper App. C.3 — worth +69.5% there)",
|
|
170
|
+
"Orchestrate; don't solve. Loop: `search`/`grep_context`/`outline` to find the slice →",
|
|
171
|
+
"`map_files` / `llm_query_batched` to read it → memoize into `answers` → assemble by lookup.",
|
|
172
|
+
"`answers` and `plan` persist across turns and snapshots: **if a value isn't in `answers`, it",
|
|
173
|
+
"doesn't exist** — never reuse a number from your own earlier reasoning or truncated stdout.",
|
|
174
|
+
"Red flags: printing file bodies to read them; regex used to infer meaning rather than match",
|
|
175
|
+
"a literal; two turns into an analysis with zero sub-LLM calls; one sub-call per file instead",
|
|
176
|
+
"of one batch. Exception — AUTHORING is not reading: you write every edit body yourself.",
|
|
177
|
+
].join("\n");
|
|
178
|
+
|
|
72
179
|
function howToRunCode(): string {
|
|
73
180
|
return [
|
|
74
181
|
"To run Python, write a fenced ```repl``` block. The REPL **persists** across turns. Only",
|
|
@@ -100,24 +207,24 @@ function replGlossary(
|
|
|
100
207
|
" bodies into your own output.",
|
|
101
208
|
CONTEXT_EXCLUSION_NOTE,
|
|
102
209
|
"",
|
|
103
|
-
"
|
|
210
|
+
" Worked example — find the slice, then delegate it:",
|
|
104
211
|
" ```python",
|
|
105
|
-
|
|
106
|
-
" for
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
" f\"Analyze {f['path']} ({f['tokens']} tok):\\n{f['content']}\"",
|
|
110
|
-
" for f in batch",
|
|
111
|
-
" ])",
|
|
212
|
+
' hits = search("where is the retry/backoff policy configured?", k=8)',
|
|
213
|
+
" paths = sorted({h['path'] for h in hits})",
|
|
214
|
+
' answers.update(map_files(paths, "Describe any retry/backoff policy in this file, with line numbers. Say NONE if absent."))',
|
|
215
|
+
" print({p: a[:80] for p, a in answers.items()})",
|
|
112
216
|
" ```",
|
|
113
217
|
);
|
|
114
218
|
}
|
|
219
|
+
lines.push(...RETRIEVAL_GLOSSARY_LINES);
|
|
115
220
|
lines.push(
|
|
116
221
|
"- `llm_query(prompt: str, model=None) -> str`: a single sub-LLM completion. Use for extraction,",
|
|
117
222
|
" summarization, or Q&A over a chunk of text.",
|
|
118
223
|
"- `llm_query_batched(prompts: list[str], model=None) -> list[str]`: run several sub-LLM calls",
|
|
119
224
|
" concurrently; output order matches input order.",
|
|
120
225
|
...CHUNKED_GLOSSARY_LINES,
|
|
226
|
+
...SPAWN_GLOSSARY_LINES,
|
|
227
|
+
...DELEGATION_GLOSSARY_LINES,
|
|
121
228
|
);
|
|
122
229
|
if (askUserQuestion) {
|
|
123
230
|
lines.push(
|
|
@@ -189,6 +296,8 @@ function replGlossary(
|
|
|
189
296
|
);
|
|
190
297
|
}
|
|
191
298
|
lines.push(
|
|
299
|
+
"- `answers` / `plan`: two dicts that persist across turns and snapshots. Memoize every",
|
|
300
|
+
" verified result in `answers` — see the decomposition doctrine below.",
|
|
192
301
|
"- `SHOW_VARS() -> str`: list every variable currently in the REPL.",
|
|
193
302
|
'- `answer`: a dict initialized to {"content": "", "ready": False}. To submit your final answer,',
|
|
194
303
|
' set `answer["content"]` to the answer text and `answer["ready"] = True`.',
|
|
@@ -244,7 +353,9 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
244
353
|
"Start by probing `context` (print a few lines, count items). Then build up an answer to the query.",
|
|
245
354
|
];
|
|
246
355
|
if (opts.orchestrator ?? true) {
|
|
247
|
-
|
|
356
|
+
// Two counterweights, both required (paper App. B): the addendum bounds OVER-recursion
|
|
357
|
+
// (batching/cost), ENV_TIPS bounds UNDER-recursion (solving it yourself).
|
|
358
|
+
parts.push("", orchestratorAddendum(maxPromptChars), "", ENV_TIPS);
|
|
248
359
|
}
|
|
249
360
|
if (kind === "files") {
|
|
250
361
|
parts.push("", LARGE_FILE_RULE_LINES.join("\n"));
|
|
@@ -263,61 +374,56 @@ function nativeReplGlossary(): string {
|
|
|
263
374
|
"",
|
|
264
375
|
"### REPL Environment",
|
|
265
376
|
"- `context`: list[dict] — every file in the repository. Each dict: `path` (str), `content` (str), `tokens` (int).",
|
|
377
|
+
"",
|
|
378
|
+
"Retrieval — free (no sub-LLM call, no tokens). **Start here, before guessing filenames:**",
|
|
379
|
+
"- `search(query, k=10, path_glob=None) -> [{path, line, score, snippet}]` — BM25 over `context`. Returns pointers, not bodies.",
|
|
380
|
+
"- `grep_context(pattern, k=50, path_glob=None, before=0, after=0) -> {hits, counts, total, truncated}` — regex; `counts` stays complete when `hits` is capped. Lexical needles only.",
|
|
381
|
+
"- `outline(path) -> str` — definition/heading skeleton with line numbers. Orient in ~200 chars instead of printing 20K.",
|
|
382
|
+
"",
|
|
383
|
+
"Delegation — everything semantic goes through these:",
|
|
384
|
+
"- `map_files(files, prompt, model=None) -> {path: answer}` — ask `prompt` of many files; batches and splits oversized files for you. **The default way to read many files.**",
|
|
385
|
+
"- `llm_map_reduce(items, map_prompt, reduce_prompt, model=None) -> str` — map in one batch, then reduce with one call.",
|
|
266
386
|
"- `llm_query(prompt, model=None) -> str` — one-shot sub-LLM. Use for extraction, summarization, Q&A over a chunk.",
|
|
267
387
|
"- `llm_query_batched(prompts, model=None) -> list[str]` — concurrent sub-LLM calls; output order matches input order.",
|
|
268
388
|
CHUNKED_GLOSSARY_LINE_NATIVE,
|
|
269
389
|
"- `rlm_query(prompt, model=None) -> str` — recursive RLM with its own REPL for complex sub-tasks needing iterative reasoning. Prefer llm_query — rlm_query is slower and costlier.",
|
|
270
390
|
"- `rlm_query_batched(prompts, model=None) -> list[str]` — concurrent recursive RLM calls.",
|
|
391
|
+
"- `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.",
|
|
392
|
+
"",
|
|
271
393
|
"",
|
|
394
|
+
"- `answers` / `plan` — dicts persisted across every repl() call and snapshot. Your memo.",
|
|
272
395
|
"- `todo(action, **kwargs) -> str` — manage a task list. Actions: create, update, list, get, delete, clear. Statuses: pending → in_progress → completed.",
|
|
273
396
|
"- `load_library(source) -> dict`: append external dir/file/git tree into `context` under `lib/<id>/…`. Return is metadata only — always use `context`.",
|
|
274
397
|
"- `SHOW_VARS() -> str` — list all variables currently in the REPL.",
|
|
275
398
|
"- `answer`: dict `{\"content\": \"\", \"ready\": False}`. Setting `answer[\"ready\"] = True` delivers it to the user; do not restate it.",
|
|
276
399
|
"",
|
|
277
|
-
|
|
278
|
-
"You are an **orchestrator, not a solver**. After probing `context`, decompose the task into sub-LLM / REPL steps,",
|
|
279
|
-
"then execute one step at a time, printing samples of each result to verify before moving on.",
|
|
400
|
+
ENV_TIPS_CONDENSED,
|
|
280
401
|
"",
|
|
281
|
-
"
|
|
282
|
-
"`llm_query` / `llm_query_batched` — never dump raw file bodies into your own output. Aggregate small",
|
|
283
|
-
"results back in Python. Use Python string operations (`in`, `re.search`) over `context` for quick lookups.",
|
|
284
|
-
"",
|
|
285
|
-
"### Chunking Strategy",
|
|
402
|
+
"### Worked pattern",
|
|
286
403
|
"```python",
|
|
287
|
-
|
|
288
|
-
"for
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
" f\"Analyze {f['path']} ({f['tokens']} tok):\\n{f['content']}\"",
|
|
292
|
-
" for f in batch",
|
|
293
|
-
" ])",
|
|
294
|
-
" # aggregate results into a buffer",
|
|
404
|
+
'hits = search("where is retry/backoff configured?", k=8)',
|
|
405
|
+
"paths = sorted({h['path'] for h in hits})",
|
|
406
|
+
'answers.update(map_files(paths, "Describe any retry/backoff policy here, with line numbers. Say NONE if absent."))',
|
|
407
|
+
"print({p: a[:80] for p, a in answers.items()})",
|
|
295
408
|
"```",
|
|
296
|
-
`-
|
|
409
|
+
`- 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.`,
|
|
297
410
|
"",
|
|
298
411
|
"### Choosing Between Tools",
|
|
299
412
|
"| Tool | When |",
|
|
300
413
|
"|------|------|",
|
|
301
|
-
"| `repl({code})` |
|
|
302
|
-
"| `
|
|
303
|
-
"| `
|
|
304
|
-
"| `
|
|
305
|
-
"| `
|
|
306
|
-
"| `rlm_query` (
|
|
307
|
-
"| `todo` (
|
|
308
|
-
"",
|
|
309
|
-
"### Workflow",
|
|
310
|
-
"1. **Plan**: Create todos for the multi-step analysis. Probe `context` — print length, inspect a few entries.",
|
|
311
|
-
"2. **Chunk & Delegate**: Slice `context` into batches, delegate each batch to sub-LLMs via `llm_query_batched`.",
|
|
312
|
-
"3. **Aggregate**: Collect results in Python, pass aggregated results to a final `llm_query` or produce the answer directly.",
|
|
313
|
-
"4. **Finalize**: For file changes, call the native `edit` / `write` tools directly — you",
|
|
314
|
-
" author the change, and Pi validates the anchor and renders the diff. For analysis tasks,",
|
|
315
|
-
" write a normal message.",
|
|
414
|
+
"| `repl({code})` | ALL repository reading, search, and analysis; Python scripting; state across calls |",
|
|
415
|
+
"| `edit` / `write` | Change or create a file. Compose oldText/newText yourself; exact match required |",
|
|
416
|
+
"| `search` / `grep_context` / `outline` (in repl) | Locate the relevant slice — free, do this first |",
|
|
417
|
+
"| `zebra-mcp` | Semantic/embedding search when lexical `search` misses the concept |",
|
|
418
|
+
"| `map_files` / `llm_query_batched` (in repl) | Read/extract/classify that slice |",
|
|
419
|
+
"| `rlm_query` (in repl) | Sub-task needing its own iterative reasoning and REPL |",
|
|
420
|
+
"| `todo` (in repl) | Track multi-step progress visibly to the user |",
|
|
316
421
|
"",
|
|
317
422
|
"### Task-Specific Patterns",
|
|
318
423
|
LARGE_FILE_RULE_NATIVE,
|
|
319
|
-
"- Architecture/code review:
|
|
320
|
-
"- Bug investigation:
|
|
424
|
+
"- Architecture/code review: `search` for the subsystem, then `map_files` the hits.",
|
|
425
|
+
"- Bug investigation: `grep_context` for the literal symbol/message, then `map_files` the matching files.",
|
|
426
|
+
"- Finalizing: for file changes call `edit`/`write` directly so Pi validates the anchor and renders the diff; for analysis, write a normal message.",
|
|
321
427
|
"- If sub-LLM credits are exhausted, report partial results and stop — do not bypass REPL restrictions.",
|
|
322
428
|
"",
|
|
323
429
|
"Reserve your own tokens for high-level decisions: what to ask next, how to combine sub-LLM outputs, when to finalize.",
|
|
@@ -336,9 +442,10 @@ export function buildNativeSystemPrompt(): string {
|
|
|
336
442
|
"- `read`/`grep` are blocked; bash readers (cat/sed/head/tail/awk/rg) are blocked; bash output is hard-capped at 4K chars.",
|
|
337
443
|
"- repl() stdout returned to you is hard-capped at 4K chars — printing file bodies is USELESS; the text will not reach you.",
|
|
338
444
|
"",
|
|
339
|
-
"
|
|
340
|
-
"
|
|
341
|
-
"
|
|
445
|
+
"LOCATE-THEN-DELEGATE: `search(query)` / `grep_context(pattern)` / `outline(path)` cost nothing",
|
|
446
|
+
"— run them FIRST to find the relevant slice. Then, if a step needs MEANING from more than ~4K",
|
|
447
|
+
"chars, that reading MUST be a map_files / llm_query / llm_query_batched / llm_query_chunked",
|
|
448
|
+
"call (rlm_query for iterative sub-tasks). Deterministic Python over `context` is free and",
|
|
342
449
|
"preferred for lookups. Semantic reading is always delegated.",
|
|
343
450
|
"",
|
|
344
451
|
"All file content is pre-loaded in the REPL `context` variable. Use ONLY `repl({code})`.",
|
|
@@ -355,8 +462,12 @@ export function buildNativeSystemPrompt(): string {
|
|
|
355
462
|
}
|
|
356
463
|
|
|
357
464
|
/** Soft cap on the static native prompt. Leaves headroom for per-turn context injection
|
|
358
|
-
* without bloating the root model's system prompt. Exceeded → phase-guards.ts fails.
|
|
359
|
-
|
|
465
|
+
* without bloating the root model's system prompt. Exceeded → phase-guards.ts fails.
|
|
466
|
+
* Raised from 6K when the retrieval glossary and the condensed decomposition doctrine
|
|
467
|
+
* landed; both buy far more than they cost (paper Table 2, Fig. 4a), then again for
|
|
468
|
+
* spawn/rlm_await: the async fan-out API is part of the model-visible contract, and
|
|
469
|
+
* ~50 tokens is worth the model actually using it. */
|
|
470
|
+
export const NATIVE_PROMPT_BUDGET = 7_700;
|
|
360
471
|
|
|
361
472
|
/** Exported for tests — prompt length without context metadata (which is injected separately). */
|
|
362
473
|
export const NATIVE_PROMPT_STATIC = buildNativeSystemPrompt();
|
|
@@ -365,10 +476,11 @@ export const NATIVE_PROMPT_STATIC = buildNativeSystemPrompt();
|
|
|
365
476
|
export const NATIVE_TURN_REMINDER = [
|
|
366
477
|
"[RLM orchestrator contract — enforced by the runtime, not optional:",
|
|
367
478
|
"repl() stdout to you is hard-capped at 4K chars; read/grep and bash readers are blocked.",
|
|
368
|
-
"
|
|
369
|
-
"
|
|
370
|
-
"
|
|
371
|
-
"
|
|
479
|
+
"LOCATE FIRST with search() / grep_context() / outline() — they cost nothing. Any SEMANTIC",
|
|
480
|
+
"reading MUST then go through map_files / llm_query / llm_query_batched / llm_query_chunked",
|
|
481
|
+
"(rlm_query for iterative sub-tasks). Memoize what you reuse in `answers`; a value not in",
|
|
482
|
+
"`answers` does not exist. AUTHORING IS NOT READING: you write every edit body yourself and",
|
|
483
|
+
"apply it with the native `edit` / `write` tools — never delegate code you will ship.",
|
|
372
484
|
"Keep your own output to decisions, authored edits, and aggregation.]",
|
|
373
485
|
].join("\n");
|
|
374
486
|
|
package/src/prompts/user.ts
CHANGED
|
@@ -7,12 +7,8 @@ export function buildTurnPrompt(
|
|
|
7
7
|
iteration: number,
|
|
8
8
|
maxIterations: number,
|
|
9
9
|
gateMessage?: string,
|
|
10
|
-
phaseGuidanceText?: string,
|
|
11
10
|
): string {
|
|
12
|
-
const
|
|
13
|
-
if (phaseGuidanceText) parts.push(phaseGuidanceText);
|
|
14
|
-
if (gateMessage) parts.push(gateMessage);
|
|
15
|
-
const prefix = parts.length > 0 ? `${parts.join("\n\n")}\n\n` : "";
|
|
11
|
+
const prefix = gateMessage ? `${gateMessage}\n\n` : "";
|
|
16
12
|
const body = `Turn ${iteration + 1}/${maxIterations}:`;
|
|
17
13
|
if (iteration === 0) {
|
|
18
14
|
return (
|
package/src/sandbox/protocol.ts
CHANGED
|
@@ -52,9 +52,6 @@ export interface WorkerResponse {
|
|
|
52
52
|
readonly var_names?: readonly string[];
|
|
53
53
|
// load_context:
|
|
54
54
|
readonly index?: number;
|
|
55
|
-
// snapshot/restore:
|
|
56
|
-
readonly skipped?: readonly string[];
|
|
57
|
-
readonly restored?: readonly string[];
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
/** Kinds of sub-LLM interrupt the worker can raise mid-exec. */
|
|
@@ -88,13 +85,15 @@ export interface AskAnswer {
|
|
|
88
85
|
readonly custom?: string;
|
|
89
86
|
}
|
|
90
87
|
|
|
91
|
-
export interface AskUserQuestionReply {
|
|
92
|
-
readonly answers: readonly AskAnswer[];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
88
|
interface InterruptBase {
|
|
96
89
|
readonly rid: string;
|
|
97
90
|
readonly depth: number;
|
|
91
|
+
/**
|
|
92
|
+
* Started via `spawn()`: the request may outlive the `exec` that issued it, so the host
|
|
93
|
+
* must not attach it to that invocation's emitter or LimitGuard. Absent on the
|
|
94
|
+
* synchronous path.
|
|
95
|
+
*/
|
|
96
|
+
readonly detached?: boolean;
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
interface PromptInterrupt extends InterruptBase {
|
|
@@ -14,6 +14,8 @@ export interface SandboxManagerConfig {
|
|
|
14
14
|
readonly python: string;
|
|
15
15
|
readonly sandboxInitTimeoutMs: number;
|
|
16
16
|
readonly maxPromptChars: number;
|
|
17
|
+
/** Max seconds the worker waits for a host reply while parked in rlm_await. */
|
|
18
|
+
readonly awaitTimeoutS: number;
|
|
17
19
|
readonly signal?: AbortSignal;
|
|
18
20
|
readonly onSandboxDiscarded?: () => void;
|
|
19
21
|
}
|
|
@@ -61,6 +63,7 @@ export class SandboxManager {
|
|
|
61
63
|
signal: this.config.signal,
|
|
62
64
|
initTimeoutMs: this.config.sandboxInitTimeoutMs,
|
|
63
65
|
maxPromptChars: this.config.maxPromptChars,
|
|
66
|
+
awaitTimeoutS: this.config.awaitTimeoutS,
|
|
64
67
|
handlers,
|
|
65
68
|
}).then(async (s) => {
|
|
66
69
|
// Load context on first creation if available.
|
|
@@ -81,23 +84,23 @@ export class SandboxManager {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
|
-
* Execute code in the sandbox. Serializes concurrent calls via
|
|
85
|
-
* (second call waits for first
|
|
86
|
-
*
|
|
87
|
+
* Execute code in the sandbox with no per-invocation setup. Serializes concurrent calls via
|
|
88
|
+
* a promise queue (second call waits for the first, no interleaving). On failure the sandbox
|
|
89
|
+
* is nullified so the next call recreates it (death-recreate).
|
|
87
90
|
*/
|
|
88
|
-
async exec(code: string): Promise<ReplResult> {
|
|
89
|
-
return this.execQueued(code);
|
|
91
|
+
async exec(code: string, signal?: AbortSignal): Promise<ReplResult> {
|
|
92
|
+
return this.execQueued(code, undefined, signal);
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
/**
|
|
93
|
-
* Execute code after running setup inside the serialized execution slot
|
|
94
|
-
*
|
|
96
|
+
* Execute code after running `setup` inside the serialized execution slot, so per-invocation
|
|
97
|
+
* handler state (emitter, limits, depth) always matches the active REPL run.
|
|
95
98
|
*/
|
|
96
|
-
async execWithSetup(code: string, setup: () => void): Promise<ReplResult> {
|
|
97
|
-
return this.execQueued(code, setup);
|
|
99
|
+
async execWithSetup(code: string, setup: () => void, signal?: AbortSignal): Promise<ReplResult> {
|
|
100
|
+
return this.execQueued(code, setup, signal);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
private async execQueued(code: string, setup?: () => void): Promise<ReplResult> {
|
|
103
|
+
private async execQueued(code: string, setup?: () => void, signal?: AbortSignal): Promise<ReplResult> {
|
|
101
104
|
if (!this.sandbox) throw new Error("Sandbox not initialized — call getOrCreate first");
|
|
102
105
|
|
|
103
106
|
// Serialize: queue behind any in-flight execution
|
|
@@ -111,7 +114,7 @@ export class SandboxManager {
|
|
|
111
114
|
const sandbox = this.sandbox;
|
|
112
115
|
if (!sandbox) throw new Error("Sandbox not initialized — previous execution disposed it");
|
|
113
116
|
setup?.();
|
|
114
|
-
return await sandbox.exec(code);
|
|
117
|
+
return await sandbox.exec(code, signal);
|
|
115
118
|
} catch (err) {
|
|
116
119
|
// Death-recreate: worker died — nullify so next repl() recreates
|
|
117
120
|
if (this.sandbox) {
|
|
@@ -128,6 +131,17 @@ export class SandboxManager {
|
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Keep the live sandbox's request watchdog from firing while it is legitimately idle.
|
|
136
|
+
*
|
|
137
|
+
* The watchdog only refreshes on frames arriving at THIS sandbox, but a detached
|
|
138
|
+
* rlm_query child does its work in its own sandbox — so without a heartbeat a healthy
|
|
139
|
+
* long-running child would trip death-recreate and destroy the REPL namespace.
|
|
140
|
+
*/
|
|
141
|
+
refreshWatchdog(): void {
|
|
142
|
+
this.sandbox?.refreshWatchdog();
|
|
143
|
+
}
|
|
144
|
+
|
|
131
145
|
/** True if the sandbox is alive and not disposed. */
|
|
132
146
|
get isAlive(): boolean {
|
|
133
147
|
return this.sandbox !== null && !this.disposed;
|