@hicaru/pi-rlm 0.3.6 → 0.3.9
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 +2 -2
- package/package.json +1 -1
- package/src/bridge/handlers/emitting.ts +5 -23
- package/src/bridge/handlers/index.ts +1 -1
- package/src/bridge/handlers/llm-query.ts +84 -29
- package/src/bridge/handlers/rlm-query.ts +133 -33
- package/src/bridge/handlers/types.ts +12 -0
- package/src/commands/pins.ts +51 -0
- package/src/commands/rlm-config.ts +4 -88
- package/src/commands/rlm-llm.ts +59 -0
- package/src/commands/rlm-rlm.ts +58 -0
- package/src/commands/rlm.ts +2 -2
- package/src/config/defaults.ts +17 -0
- package/src/config/settings.ts +58 -5
- package/src/core/answer.ts +7 -10
- package/src/core/budget.ts +182 -0
- package/src/core/compaction.ts +46 -0
- package/src/core/engine.ts +185 -5
- package/src/core/iteration.ts +5 -0
- package/src/core/ledger.ts +343 -0
- package/src/core/memory.ts +589 -0
- package/src/core/model-registry.ts +88 -0
- package/src/core/types.ts +44 -3
- package/src/index.ts +107 -12
- package/src/mode/rlm-mode.ts +58 -10
- package/src/prompts/glossary.ts +147 -57
- package/src/prompts/native.ts +12 -7
- package/src/prompts/system.ts +22 -7
- package/src/prompts/user.ts +6 -3
- package/src/sandbox/interrupts.ts +24 -0
- package/src/sandbox/protocol.ts +69 -5
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +11 -6
- package/src/sandbox/py/scaffold.py +615 -0
- package/src/sandbox/py/worker.py +53 -506
- package/src/sandbox/sandbox.ts +21 -3
- package/src/text/repl-output.ts +15 -0
- package/src/tool/repl-render.ts +4 -10
- package/src/tool/repl-result.ts +54 -10
- package/src/tool/repl-tool.ts +50 -3
- package/src/tool/rlm-aggregator.ts +16 -3
- package/src/tool/rlm-details.ts +7 -0
- package/src/tool/rlm-events.ts +17 -1
- package/src/tool/rlm-tool.ts +25 -14
- package/src/tool/subcall-render.ts +14 -129
- package/src/tool/subcall-store.ts +11 -1
- package/src/ui/intro.ts +13 -4
- package/src/ui/modal/agent-modal.ts +104 -0
- package/src/ui/modal/modal-view.ts +132 -0
- package/src/ui/modal/timeline-store.ts +85 -0
- package/src/ui/model-picker/drilldown.ts +173 -0
- package/src/ui/model-picker/grouping.ts +81 -0
- package/src/ui/model-picker/levels.ts +63 -0
- package/src/ui/model-picker.ts +7 -197
- package/src/ui/panel/run-registry.ts +135 -0
- package/src/ui/panel/tree-panel.ts +46 -0
- package/src/ui/status.ts +26 -10
- package/src/ui/theme.ts +0 -4
- package/src/ui/tree/tree-model.ts +221 -0
- package/src/ui/tree/tree-rows.ts +73 -0
- package/src/ui/tree/tree-widget.ts +186 -0
- package/src/util/concurrency.ts +47 -0
package/src/prompts/glossary.ts
CHANGED
|
@@ -39,6 +39,16 @@ export const RETRIEVAL_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
39
39
|
" Orient in ~200 chars instead of printing 20K. Matches exact path, then suffix, then glob.",
|
|
40
40
|
]);
|
|
41
41
|
|
|
42
|
+
/** v5 delegation doctrine (audit C5): children have NO retrieval tools — their world is the
|
|
43
|
+
* sliced `context` they were handed. This REPLACES the retrieval lines in child prompts so
|
|
44
|
+
* the prompt and the runtime sandbox agree (a child taught to `search` burns turns on NameError). */
|
|
45
|
+
export const DELEGATION_SURFACE_LINES: readonly string[] = Object.freeze([
|
|
46
|
+
"- **No `search` / `grep_context` / `outline` / `add_context` in this REPL** (delegation",
|
|
47
|
+
" surface, v5 doctrine): your task arrived WITH its world in `context`. Explore it with",
|
|
48
|
+
" Python (list comprehensions, string matching, slicing) and delegate slices to sub-LLMs —",
|
|
49
|
+
" never re-ask the parent for retrieval.",
|
|
50
|
+
]);
|
|
51
|
+
|
|
42
52
|
/** One-line delegation helpers — orchestrating must be cheaper than solving. */
|
|
43
53
|
export const DELEGATION_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
44
54
|
"- `map_files(files, prompt) -> Task`: always spawn. `await_task(t)` → dict[path, answer].",
|
|
@@ -57,12 +67,21 @@ export const CHUNKED_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
57
67
|
|
|
58
68
|
/** Non-blocking fan-out: spawn now, collect later (headless glossary). */
|
|
59
69
|
export const SPAWN_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
60
|
-
"- **ALWAYS SPAWN (Task +
|
|
70
|
+
"- **ALWAYS SPAWN (Task + ↗bg):** `llm_query` / `llm_batch` / `rlm_query` / `rlm_batch` /",
|
|
61
71
|
" `map_files` / `llm_query_chunked`. Never treat the return as the answer.",
|
|
62
|
-
" Collect with `await_task(t)
|
|
72
|
+
" Collect with `await_task(t)`, `await_task([t1,t2,…])`, or `await_task()` (every still-running Task).",
|
|
73
|
+
" If `await_task` returns `Error: sub-call still running`, call it again — do not respawn.",
|
|
74
|
+
" `list_tasks()` → [{kind, label, done, var}]. Fire independent Tasks first, free work, then await.",
|
|
63
75
|
" Do NOT await after every independent spawn (serializes wall time). `task.done` when settled.",
|
|
76
|
+
"- `[ledger]` global state: the blackboard in your prompt lists inflight/done agent claims.",
|
|
77
|
+
" NEVER `rlm_query` a task already on `[ledger]` (await it / reuse the result); ancestor",
|
|
78
|
+
" echo is rejected with a stub. `list_claims()` shows the live table anytime.",
|
|
64
79
|
"- `spawn(fn, *args) -> Task`: same as calling the always-spawn tools (not `llm_map_reduce`).",
|
|
65
80
|
"- Only `llm_map_reduce` still blocks until done.",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/** v5 (audit C5): the spawn worked example, retrieval flavor — root surface only. */
|
|
84
|
+
export const SPAWN_EXAMPLE_RETRIEVAL: readonly string[] = Object.freeze([
|
|
66
85
|
"",
|
|
67
86
|
" ```python",
|
|
68
87
|
" # Multi-area study: one rlm_batch (parallel workers), free locate, then await",
|
|
@@ -76,11 +95,20 @@ export const SPAWN_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
76
95
|
" ```",
|
|
77
96
|
]);
|
|
78
97
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
/** v5 (audit C5): the spawn worked example, delegation flavor — no retrieval, slice instead. */
|
|
99
|
+
export const SPAWN_EXAMPLE_DELEGATION: readonly string[] = Object.freeze([
|
|
100
|
+
"",
|
|
101
|
+
" ```python",
|
|
102
|
+
" # Multi-area study: one rlm_batch (parallel workers), slice your world while they run",
|
|
103
|
+
" t = rlm_batch([",
|
|
104
|
+
" \"Answer from the FIRST half of the context only: paths + symbols for X.\",",
|
|
105
|
+
" \"Answer from the SECOND half only: report how Y is configured.\",",
|
|
106
|
+
" ])",
|
|
107
|
+
" half = [f['path'] for f in context[:len(context)//2]] # free work while Tasks run",
|
|
108
|
+
" reports = await_task(t)",
|
|
109
|
+
" # One-shot extracts: map_files / llm_batch also return Task → await_task",
|
|
110
|
+
" ```",
|
|
111
|
+
]);
|
|
84
112
|
export const RECURSION_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
85
113
|
"",
|
|
86
114
|
" **What a child sees:** it inherits YOUR `context` — every file you have loaded, including",
|
|
@@ -95,6 +123,19 @@ export const RECURSION_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
|
95
123
|
" this section disappears at the last recursive depth.",
|
|
96
124
|
]);
|
|
97
125
|
|
|
126
|
+
/** v5 recursion section, delegation variant (audit C5): describes what a delegation child
|
|
127
|
+
* receives — the narrowed pack as text, no retrieval of its own. */
|
|
128
|
+
export const RECURSION_DELEGATION_LINES: readonly string[] = Object.freeze([
|
|
129
|
+
"",
|
|
130
|
+
" **What a child sees:** it inherits YOUR `context` (narrowed by `paths=` when given) and works",
|
|
131
|
+
" on it as text — it has NO retrieval tools, so put what matters in your prompt and `paths`,",
|
|
132
|
+
" never file bodies you already share (that costs tokens twice and buys nothing).",
|
|
133
|
+
" Inheritance is one-way: sources the child loads, and its whole REPL, die with it — only its",
|
|
134
|
+
" final answer string returns. The child cannot write to your `answers` or `plan`.",
|
|
135
|
+
" At the depth cap `rlm_query` degrades to a plain sub-LLM call with NO context, which is why",
|
|
136
|
+
" this section disappears at the last recursive depth.",
|
|
137
|
+
]);
|
|
138
|
+
|
|
98
139
|
/**
|
|
99
140
|
* Sub-RLM orientation. Emitted only at depth > 0, where `context` is the parent's world rather
|
|
100
141
|
* than a repository the run packed for itself.
|
|
@@ -146,38 +187,52 @@ export const LARGE_FILE_RULE_NATIVE =
|
|
|
146
187
|
* The paper is explicit (App. B) that one prompt does not port across models and that both
|
|
147
188
|
* guardrails are needed; keep them both.
|
|
148
189
|
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
/** Decomposition doctrine. `delegation` drops retrieval names (audit R3) — children
|
|
191
|
+
* have no `search` and must not be told to locate with it. */
|
|
192
|
+
export function envTips(delegation = false): string {
|
|
193
|
+
const locate = delegation
|
|
194
|
+
? "Your job: (1) slice `context` with Python (indexing, string matching, comprehensions),"
|
|
195
|
+
: "Your job: (1) free locate with `search` / `grep_context` / `outline`,";
|
|
196
|
+
const probe = delegation
|
|
197
|
+
? "1. Probe: `print(len(context))`; locate targets with Python slicing / string matching. Do not print file bodies."
|
|
198
|
+
: "1. Probe: `print(len(context))`; locate targets with `search` when your surface has it, else\n Python slicing / string matching. Do not print file bodies.";
|
|
199
|
+
return [
|
|
200
|
+
"## Decomposition doctrine",
|
|
201
|
+
"",
|
|
202
|
+
"**Orchestrate; don't solve.** A single chain of thought over a large repository drifts —",
|
|
203
|
+
"you lose partials and compound mistakes. Sub-workers are competent: trust them; don't read for them.",
|
|
204
|
+
"",
|
|
205
|
+
locate,
|
|
206
|
+
"(2) fan out: **multi-step areas → `rlm_batch` / `rlm_query`**; one-shot extracts →",
|
|
207
|
+
" `map_files` / `llm_batch` (all return Task — `await_task` for content),",
|
|
208
|
+
"(3) memoize into `answers`, (4) sanity-check before dependents, (5) assemble from `answers`.",
|
|
209
|
+
"Your own compute is: pointers, dict lookups, string formatting, and decisions.",
|
|
210
|
+
"",
|
|
211
|
+
"### The only state that matters",
|
|
212
|
+
"`answers` and `plan` are dicts that persist across every turn.",
|
|
213
|
+
"**If a result isn't in `answers`, you have not memoized it.** Task handles are REPL vars —",
|
|
214
|
+
"`list_tasks()` / `SHOW_VARS()` find them. Do not trust truncated stdout. Memoize after await_task.",
|
|
215
|
+
"",
|
|
216
|
+
"### Shape of a run",
|
|
217
|
+
probe,
|
|
218
|
+
"2. Plan: sub-questions into `plan` (each from a named slice / module).",
|
|
219
|
+
"3. Fan out **in parallel**: one `rlm_batch` for independent multi-step studies, or",
|
|
220
|
+
" `map_files` / `llm_batch` for one-shot reads — not one serial call per file.",
|
|
221
|
+
"4. Assemble from `answers`.",
|
|
222
|
+
"",
|
|
223
|
+
"### Red flags — you are off track",
|
|
224
|
+
"- Printing file bodies / native bulk read → stop; use map_files or rlm_*.",
|
|
225
|
+
"- `llm_query(\"Read src/foo.ts…\")` with only a path — sub-LLM has **no disk**; use map_files/rlm_*.",
|
|
226
|
+
"- Multi-module task with zero `rlm_batch`/`rlm_query`/`map_files` → under-delegating.",
|
|
227
|
+
"- Await after every independent spawn → serializes wall time; fire-all-then-await.",
|
|
228
|
+
"- Treating Task as the answer without `await_task`.",
|
|
229
|
+
"- Regex used to *infer meaning* → sub-LLM job. Regex is for exact needles only.",
|
|
230
|
+
"- Two turns with zero sub-LLM calls on analysis → solving it yourself.",
|
|
231
|
+
].join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Root-surface doctrine (back-compat alias of `envTips(false)`). */
|
|
235
|
+
export const ENV_TIPS = envTips(false);
|
|
181
236
|
|
|
182
237
|
/** Native-mode variant of the doctrine — same rules, sized for the native prompt budget. */
|
|
183
238
|
export const ENV_TIPS_CONDENSED = [
|
|
@@ -185,7 +240,7 @@ export const ENV_TIPS_CONDENSED = [
|
|
|
185
240
|
"Orchestrate; don't solve. Free locate → fan-out Tasks → await_task → memoize in `answers`.",
|
|
186
241
|
"Multi-module / multi-step areas: **`rlm_batch` (or rlm_query)** — not serial native read.",
|
|
187
242
|
"One-shot extracts: `map_files` / `llm_batch`. Always Task → await_task; fire-all then await.",
|
|
188
|
-
"`answers`/`plan` persist
|
|
243
|
+
"`answers`/`plan` persist collected results. Task handles are REPL vars (`list_tasks()` / `SHOW_VARS()`).",
|
|
189
244
|
"Red flags: bulk file dumps; llm_query with path-only (no content — no disk!); zero rlm_*/map_files",
|
|
190
245
|
"on multi-area tasks; await after each spawn; Task treated as answer.",
|
|
191
246
|
"AUTHORING: you write every edit body yourself.",
|
|
@@ -204,6 +259,7 @@ export function replGlossary(
|
|
|
204
259
|
recursion: boolean,
|
|
205
260
|
contextLoader: boolean,
|
|
206
261
|
child: boolean,
|
|
262
|
+
delegation = false,
|
|
207
263
|
): string {
|
|
208
264
|
const lines = ["Available in the REPL:"];
|
|
209
265
|
if (kind === "text") {
|
|
@@ -221,19 +277,36 @@ export function replGlossary(
|
|
|
221
277
|
CONTEXT_EXCLUSION_NOTE,
|
|
222
278
|
);
|
|
223
279
|
if (child) lines.push(...CHILD_CONTEXT_LINES);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
280
|
+
if (delegation) {
|
|
281
|
+
lines.push(
|
|
282
|
+
"",
|
|
283
|
+
" Worked example — slice the world you were handed, then delegate it (Task + await):",
|
|
284
|
+
" ```python",
|
|
285
|
+
" slice = [f for f in context if f['path'].startswith('src/auth/')][:6]",
|
|
286
|
+
" prompts = [f\"Answer from this file only.\\n\\n{f['content'][:4000]}\" for f in slice]",
|
|
287
|
+
" t = llm_batch(prompts)",
|
|
288
|
+
" answers.update(dict(zip([f['path'] for f in slice], await_task(t))))",
|
|
289
|
+
" ```",
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
lines.push(
|
|
293
|
+
"",
|
|
294
|
+
" Worked example — find the slice, then delegate it (Task + await):",
|
|
295
|
+
" ```python",
|
|
296
|
+
' hits = search("where is the retry/backoff policy configured?", k=8)',
|
|
297
|
+
" paths = sorted({h['path'] for h in hits})",
|
|
298
|
+
' t = map_files(paths, "Describe any retry/backoff policy in this file, with line numbers. Say NONE if absent.")',
|
|
299
|
+
" answers.update(await_task(t))",
|
|
300
|
+
" print({p: a[:80] for p, a in answers.items()})",
|
|
301
|
+
" ```",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (delegation) {
|
|
306
|
+
lines.push(...DELEGATION_SURFACE_LINES);
|
|
307
|
+
} else {
|
|
308
|
+
lines.push(...RETRIEVAL_GLOSSARY_LINES);
|
|
235
309
|
}
|
|
236
|
-
lines.push(...RETRIEVAL_GLOSSARY_LINES);
|
|
237
310
|
lines.push(
|
|
238
311
|
"- `llm_query(prompt: str) -> Task`: spawn one sub-LLM (await_task for str). The prompt must",
|
|
239
312
|
" **contain the text** to analyze — this call has no filesystem and no `context`.",
|
|
@@ -241,9 +314,10 @@ export function replGlossary(
|
|
|
241
314
|
" await_task → ordered list[str]. NEVER pass bare file paths as if the worker can open them.",
|
|
242
315
|
...CHUNKED_GLOSSARY_LINES,
|
|
243
316
|
...SPAWN_GLOSSARY_LINES,
|
|
317
|
+
...(delegation ? SPAWN_EXAMPLE_DELEGATION : SPAWN_EXAMPLE_RETRIEVAL),
|
|
244
318
|
...DELEGATION_GLOSSARY_LINES,
|
|
245
319
|
);
|
|
246
|
-
if (contextLoader) {
|
|
320
|
+
if (contextLoader && !delegation) {
|
|
247
321
|
lines.push(
|
|
248
322
|
"- `add_context(source: str) -> dict`: load a dir, file, document, or git URL and **APPEND its",
|
|
249
323
|
" files into `context`** (same shape: path/content/tokens). Documents (PDF, DOCX, XLSX, PPTX,",
|
|
@@ -264,21 +338,37 @@ export function replGlossary(
|
|
|
264
338
|
}
|
|
265
339
|
if (recursion) {
|
|
266
340
|
lines.push(
|
|
267
|
-
"- `rlm_query(task, paths=None) -> Task` / `rlm_batch(tasks, paths=None) -> Task`:",
|
|
341
|
+
"- `rlm_query(task|prompt, paths=None) -> Task` / `rlm_batch(tasks|prompts, paths=None) -> Task`:",
|
|
268
342
|
" always spawn + ↯bg. await_task for the report string(s). Child REPL is private.",
|
|
343
|
+
" Both spellings accepted; prefer `task`/`tasks`.",
|
|
269
344
|
"",
|
|
270
345
|
" **Routing (api_v5):**",
|
|
271
346
|
" - `llm_query` / `llm_batch` / `map_files` — one-shot facts/extracts (fast).",
|
|
272
|
-
|
|
347
|
+
delegation
|
|
348
|
+
? " - `rlm_query` — one multi-step study (its own delegation loop; it cannot search either)."
|
|
349
|
+
: " - `rlm_query` — one multi-step study (own search/outline loop).",
|
|
273
350
|
" - `rlm_batch` — ≥2 independent multi-step studies in **parallel** (prefer over N× rlm_query).",
|
|
274
351
|
" Always Task → await_task. Fire independent work first; never serial-await between peers.",
|
|
275
|
-
...RECURSION_CONTEXT_LINES,
|
|
352
|
+
...(delegation ? RECURSION_DELEGATION_LINES : RECURSION_CONTEXT_LINES),
|
|
276
353
|
);
|
|
277
354
|
}
|
|
278
355
|
lines.push(
|
|
279
356
|
"- `answers` / `plan`: two dicts that persist across turns. Memoize every",
|
|
280
357
|
" verified result in `answers` — see the decomposition doctrine below.",
|
|
281
|
-
"- `SHOW_VARS() -> str`: list every variable currently in the REPL.",
|
|
358
|
+
"- `SHOW_VARS() -> str`: list every variable currently in the REPL (Task handles show as `<Task …>`).",
|
|
359
|
+
"- `list_tasks()`: every Task this REPL created — [{kind, label, done, var}].",
|
|
360
|
+
...(delegation
|
|
361
|
+
? [
|
|
362
|
+
"- `memory.query(q) -> str`: durable notes under `.rlm/` that survive across sessions.",
|
|
363
|
+
" **READ-ONLY here** — `memory.add` is root-only. Query before re-studying a known area;",
|
|
364
|
+
" your own final answer is recorded as an episode automatically.",
|
|
365
|
+
]
|
|
366
|
+
: [
|
|
367
|
+
"- `memory.query(q) -> str` / `memory.add(text, paths=…, tags=…)`: durable notes under `.rlm/`",
|
|
368
|
+
" that survive across sessions. Query before re-studying a known area; add concise findings",
|
|
369
|
+
" (facts, locations, decisions) — never secrets or API keys (notes persist on disk).",
|
|
370
|
+
]),
|
|
371
|
+
"- `list_claims()`: the live `[ledger]` table of inflight/done agent work.",
|
|
282
372
|
'- `answer`: a dict initialized to {"content": "", "ready": False}. To submit your final answer,',
|
|
283
373
|
' set `answer["content"]` to the answer text and `answer["ready"] = True`.',
|
|
284
374
|
' **You MUST flip `answer["ready"] = True` — runs that never finalize are discarded.**',
|
package/src/prompts/native.ts
CHANGED
|
@@ -32,19 +32,23 @@ function nativeReplGlossary(): string {
|
|
|
32
32
|
"| `llm_batch(prompts)` | list[str] | many cheap facts in **parallel** | multi-step research |",
|
|
33
33
|
"| `map_files(files, prompt)` | dict[path,str] | same question over many files | multi-step per file |",
|
|
34
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 |",
|
|
35
|
+
"| `rlm_query(task|prompt, paths=None)` | str | **one** multi-step study | ≥2 independent studies |",
|
|
36
|
+
"| `rlm_batch(tasks|prompts, paths=None)` | list[str] | **≥2 independent studies in parallel** | trivia / one-shots |",
|
|
37
37
|
"",
|
|
38
|
-
"Collect: `await_task(t)`
|
|
38
|
+
"Collect: `await_task(t)` / `await_task([…])` / `await_task()` (all running). `list_tasks()` finds lost handles. `task.done` when settled.",
|
|
39
|
+
"If a spawn call raises, the assignment did NOT run — the variable is undefined in later cells. Re-spawn with the corrected signature from the error message.",
|
|
39
40
|
"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.**",
|
|
40
41
|
CHUNKED_GLOSSARY_LINE_NATIVE,
|
|
41
42
|
"- `llm_map_reduce(...)` **blocks** (map then reduce) — prefer Tasks when you can interleave free work.",
|
|
42
43
|
"- `spawn(fn, *args) -> Task` optional alias for always-spawn tools (not llm_map_reduce).",
|
|
43
44
|
"",
|
|
44
45
|
"### Memo / finalize",
|
|
45
|
-
"- `answers` / `plan` — persistent dicts
|
|
46
|
+
"- `answers` / `plan` — persistent dicts for **collected** results. Task handles are REPL vars (`t`), not `answers` keys.",
|
|
46
47
|
"- `add_context(source) -> dict` — append external dir/file/doc/git under `ctx/<id>/…` (metadata only).",
|
|
47
|
-
"- `
|
|
48
|
+
"- `memory.query(q)` / `memory.add(text, paths=…, tags=…)` — durable notes under `.rlm/` that survive",
|
|
49
|
+
" sessions. Query before re-studying a known area; add concise findings — never secrets or API",
|
|
50
|
+
" keys (notes persist on disk). `list_claims()` — the live `[ledger]` table of agent work.",
|
|
51
|
+
"- `SHOW_VARS()` — list REPL vars (Tasks as `<Task …>`). `list_tasks()` finds Task handles. `answer[\"ready\"]=True` only for headless finalize (native: write a normal message).",
|
|
48
52
|
"",
|
|
49
53
|
ENV_TIPS_CONDENSED,
|
|
50
54
|
"",
|
|
@@ -87,8 +91,9 @@ export function buildNativeSystemPrompt(): string {
|
|
|
87
91
|
"<contract>",
|
|
88
92
|
"Inside `repl({code})`, EVERY heavy call returns a Task immediately (not the answer):",
|
|
89
93
|
" 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
|
|
94
|
+
"ONLY `await_task(t)` / `await_task([…])` / `await_task()` returns content. Fan-out runs detached (↯bg) and outlives the cell.",
|
|
95
|
+
"If `await_task` returns `Error: sub-call still running`, call it again — do not respawn.",
|
|
96
|
+
"If you printed a Task and did not await_task, you do **not** know the answer yet. A Task handle is a REPL variable, not an `answers[]` key.",
|
|
92
97
|
"Fire independent Tasks first → free `search`/`grep_context`/`outline` → then await_task.",
|
|
93
98
|
"Do NOT await after every independent spawn (that serializes wall time).",
|
|
94
99
|
"</contract>",
|
package/src/prompts/system.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { ContextSizeStats } from "../text/tokens.ts";
|
|
|
8
8
|
import {
|
|
9
9
|
contextKindOf,
|
|
10
10
|
DEFAULT_PROMPT_CAP,
|
|
11
|
-
|
|
11
|
+
envTips,
|
|
12
12
|
howToRunCode,
|
|
13
13
|
LARGE_FILE_RULE_LINES,
|
|
14
14
|
promptCapTokensK,
|
|
@@ -33,23 +33,29 @@ export interface SystemPromptOptions {
|
|
|
33
33
|
readonly child?: boolean;
|
|
34
34
|
/** The recursion depth of this run (0 = root, 1 = first child, etc.). */
|
|
35
35
|
readonly depth?: number;
|
|
36
|
+
/** v5 doctrine: the child sandbox has the delegation-only surface (no retrieval tools). */
|
|
37
|
+
readonly delegation?: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
function orchestratorAddendum(maxPromptChars: number): string {
|
|
40
|
+
function orchestratorAddendum(maxPromptChars: number, delegation: boolean): string {
|
|
39
41
|
return [
|
|
40
42
|
"As an RLM you are an **orchestrator, not a solver**. Probe `context`, plan decomposition, then",
|
|
41
43
|
"fan out — do not solve multi-step module work yourself in a long chain of thought.",
|
|
42
44
|
"",
|
|
43
45
|
"<contract> llm_query / llm_batch / map_files / rlm_query / rlm_batch return Task (not the answer).",
|
|
44
|
-
|
|
46
|
+
delegation
|
|
47
|
+
? "Only await_task returns content. Fire independent Tasks first, slice your `context` meanwhile, then await_task."
|
|
48
|
+
: "Only await_task returns content. Fire independent Tasks first, free search, then await_task.",
|
|
45
49
|
"Do not await after every independent spawn.</contract>",
|
|
46
50
|
"",
|
|
47
51
|
"<routing> one-shot facts → llm_query/llm_batch/map_files; one multi-step study → rlm_query;",
|
|
48
52
|
"≥2 independent multi-step areas → rlm_batch (prefer over serial rlm_query). NEVER print file",
|
|
49
53
|
"bodies into your own stream when a Task tool can read them.</routing>",
|
|
50
54
|
"",
|
|
51
|
-
"Your own context window is small. Push long-context work into sub-calls.
|
|
52
|
-
|
|
55
|
+
"Your own context window is small. Push long-context work into sub-calls.",
|
|
56
|
+
delegation
|
|
57
|
+
? "If a slice of your `context` already pins a tiny fact, quote it — do not re-ask."
|
|
58
|
+
: "If free search/grep already pins a tiny fact, use that. Aggregate small results in Python / `answers`.",
|
|
53
59
|
"",
|
|
54
60
|
`Sub-call budget: (1) per-prompt < ${maxPromptChars.toLocaleString()} chars (≈${promptCapTokensK(maxPromptChars)}K tok);`,
|
|
55
61
|
"(2) ~20 prompts per llm_batch. Fat prompts in small batches beat thousands of tiny prompts.",
|
|
@@ -78,13 +84,22 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
78
84
|
`**Recursion depth: ${opts.depth}.** You are a sub-RLM — focus narrowly on your assigned`,
|
|
79
85
|
"task. Delegate (rlm_query/rlm_batch) only if the task itself must decompose further.",
|
|
80
86
|
);
|
|
87
|
+
if (opts.delegation ?? false) {
|
|
88
|
+
parts.push(
|
|
89
|
+
"",
|
|
90
|
+
"**REPL API (ONLY these):** llm_query / llm_batch / llm_query_chunked / map_files /",
|
|
91
|
+
"llm_map_reduce / rlm_query / rlm_batch / spawn / await_task / list_tasks / memory.* /",
|
|
92
|
+
"list_claims. There is no search/grep_context/outline here — your task arrived WITH its",
|
|
93
|
+
"world in `context`; slice it into llm prompts. rlm_query only for a disjoint path set.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
81
96
|
}
|
|
82
97
|
parts.push(
|
|
83
98
|
"",
|
|
84
99
|
howToRunCode(),
|
|
85
100
|
"",
|
|
86
101
|
replGlossary(
|
|
87
|
-
kind, recursion, opts.contextLoader ?? false, opts.child ?? false,
|
|
102
|
+
kind, recursion, opts.contextLoader ?? false, opts.child ?? false, opts.delegation ?? false,
|
|
88
103
|
),
|
|
89
104
|
"",
|
|
90
105
|
"REPL stdout over ~800 characters is truncated to a short excerpt — large results stay in your",
|
|
@@ -96,7 +111,7 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
96
111
|
if (opts.orchestrator ?? true) {
|
|
97
112
|
// Two counterweights, both required (paper App. B): the addendum bounds OVER-recursion
|
|
98
113
|
// (batching/cost), ENV_TIPS bounds UNDER-recursion (solving it yourself).
|
|
99
|
-
parts.push("", orchestratorAddendum(maxPromptChars), "",
|
|
114
|
+
parts.push("", orchestratorAddendum(maxPromptChars, opts.delegation ?? false), "", envTips(opts.delegation ?? false));
|
|
100
115
|
}
|
|
101
116
|
if (kind === "files") {
|
|
102
117
|
parts.push("", LARGE_FILE_RULE_LINES.join("\n"));
|
package/src/prompts/user.ts
CHANGED
|
@@ -19,7 +19,10 @@ export function buildTurnPrompt(
|
|
|
19
19
|
return `${prefix}${body}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** Asked once when the engine runs out of turns without a submitted answer.
|
|
22
|
+
/** Asked once when the engine runs out of turns without a submitted answer. Same finalize
|
|
23
|
+
* dialect as the budget wrap-up note (audit M6): answer-ready first, plain text only as an
|
|
24
|
+
* explicit fallback the engine still accepts. */
|
|
23
25
|
export const FINALIZE_PROMPT =
|
|
24
|
-
"You are out of turns.
|
|
25
|
-
|
|
26
|
+
"You are out of turns. Finalize NOW: set `answer[\"content\"]` and `answer[\"ready\"] = True` " +
|
|
27
|
+
"(fenced ```repl```) with your best final answer from everything you have gathered. " +
|
|
28
|
+
"Only if the REPL is unavailable, answer as plain text.";
|
|
@@ -51,6 +51,14 @@ export interface SubLlmHandlers {
|
|
|
51
51
|
): Promise<unknown>;
|
|
52
52
|
finishTask(summary: string, depth: number, opts: SubcallOpts): Promise<unknown>;
|
|
53
53
|
addContext(source: string, depth: number): Promise<AddContextResult>;
|
|
54
|
+
/** v5: the `[ledger]` claims table for the sandbox's `list_claims()` REPL call. */
|
|
55
|
+
ledgerClaims(): Promise<string>;
|
|
56
|
+
/** v5: durable memory surface for the sandbox's `memory.query/add/stats` object. */
|
|
57
|
+
memoryOp(
|
|
58
|
+
op: "query" | "add" | "stats",
|
|
59
|
+
args: { readonly query?: string; readonly k?: number; readonly content?: string; readonly paths?: readonly string[]; readonly tags?: readonly string[] },
|
|
60
|
+
depth: number,
|
|
61
|
+
): Promise<string>;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
function toStringArray(value: unknown): readonly string[] | undefined {
|
|
@@ -80,6 +88,8 @@ export const REJECT: SubLlmHandlers = Object.freeze({
|
|
|
80
88
|
addContext: async () => {
|
|
81
89
|
throw new Error("add_context not configured");
|
|
82
90
|
},
|
|
91
|
+
ledgerClaims: async () => UNCONFIGURED,
|
|
92
|
+
memoryOp: async () => UNCONFIGURED,
|
|
83
93
|
});
|
|
84
94
|
|
|
85
95
|
export interface ReplyBody {
|
|
@@ -337,6 +347,20 @@ export async function serviceInterrupt(
|
|
|
337
347
|
});
|
|
338
348
|
return;
|
|
339
349
|
}
|
|
350
|
+
case "ledger_claims": {
|
|
351
|
+
const table = await h.ledgerClaims();
|
|
352
|
+
reply(msg.rid, { response: table });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
case "memory": {
|
|
356
|
+
const out = await h.memoryOp(
|
|
357
|
+
msg.op,
|
|
358
|
+
{ query: msg.query, k: msg.k, content: msg.content, paths: msg.paths, tags: msg.tags },
|
|
359
|
+
d,
|
|
360
|
+
);
|
|
361
|
+
reply(msg.rid, { response: out });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
340
364
|
default: {
|
|
341
365
|
const _exhaustive: never = msg;
|
|
342
366
|
reply((_exhaustive as WorkerInterrupt).rid, {
|
package/src/sandbox/protocol.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Wire protocol for the RLM Python sandbox.
|
|
3
3
|
*
|
|
4
4
|
* Newline-delimited JSON over the worker's stdin/stdout — no sockets, no HTTP.
|
|
5
|
-
* Parent -> worker: requests (exec/load_context/shutdown)
|
|
5
|
+
* Parent -> worker: requests (exec/load_context/shutdown), llm replies, and heartbeats.
|
|
6
6
|
* Worker -> parent: request responses and mid-exec sub-LLM interrupts.
|
|
7
7
|
*
|
|
8
8
|
* Canonical api_v5 kinds only — no legacy `*_query_batched` wire names.
|
|
@@ -44,7 +44,12 @@ export interface LlmReply {
|
|
|
44
44
|
readonly error?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
/** Keep-alive while the host is working and has nothing else to write. */
|
|
48
|
+
export interface Heartbeat {
|
|
49
|
+
readonly type: "heartbeat";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ParentMessage = WorkerRequest | LlmReply | Heartbeat;
|
|
48
53
|
|
|
49
54
|
/** A normal response to a request (keyed by the request `id`). */
|
|
50
55
|
export interface WorkerResponse {
|
|
@@ -60,11 +65,13 @@ export interface WorkerResponse {
|
|
|
60
65
|
readonly execution_time?: number;
|
|
61
66
|
// user-created variable names after this exec
|
|
62
67
|
readonly var_names?: readonly string[];
|
|
68
|
+
/** Unsettled Task handles still in the worker (native pending-line / await-all). */
|
|
69
|
+
readonly pending_tasks?: readonly PendingTaskInfo[];
|
|
63
70
|
// load_context:
|
|
64
71
|
readonly index?: number;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
/** Canonical interrupt kinds (api_v5). */
|
|
74
|
+
/** Canonical interrupt kinds (api_v5 + v5 ledger + memory). */
|
|
68
75
|
export type InterruptKind =
|
|
69
76
|
| "llm_query"
|
|
70
77
|
| "rlm_query"
|
|
@@ -72,7 +79,9 @@ export type InterruptKind =
|
|
|
72
79
|
| "rlm_batch"
|
|
73
80
|
| "await"
|
|
74
81
|
| "finish"
|
|
75
|
-
| "add_context"
|
|
82
|
+
| "add_context"
|
|
83
|
+
| "ledger_claims"
|
|
84
|
+
| "memory";
|
|
76
85
|
|
|
77
86
|
interface InterruptBase {
|
|
78
87
|
readonly rid: string;
|
|
@@ -114,13 +123,31 @@ export interface AddContextInterrupt extends InterruptBase {
|
|
|
114
123
|
readonly source?: string;
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
/** v5: sandbox asks the host for the TaskLedger claims table (`list_claims()`). */
|
|
127
|
+
export interface LedgerClaimsInterrupt extends InterruptBase {
|
|
128
|
+
readonly type: "ledger_claims";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** v5: sandbox reaches the durable MemoryStore (`memory.query/add/stats`). */
|
|
132
|
+
export interface MemoryInterrupt extends InterruptBase {
|
|
133
|
+
readonly type: "memory";
|
|
134
|
+
readonly op: "query" | "add" | "stats";
|
|
135
|
+
readonly query?: string;
|
|
136
|
+
readonly k?: number;
|
|
137
|
+
readonly content?: string;
|
|
138
|
+
readonly paths?: readonly string[];
|
|
139
|
+
readonly tags?: readonly string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
117
142
|
/** A mid-exec sub-LLM/tool request from the worker. */
|
|
118
143
|
export type WorkerInterrupt =
|
|
119
144
|
| PromptInterrupt
|
|
120
145
|
| BatchInterrupt
|
|
121
146
|
| AwaitInterrupt
|
|
122
147
|
| FinishInterrupt
|
|
123
|
-
| AddContextInterrupt
|
|
148
|
+
| AddContextInterrupt
|
|
149
|
+
| LedgerClaimsInterrupt
|
|
150
|
+
| MemoryInterrupt;
|
|
124
151
|
|
|
125
152
|
export type WorkerMessage = WorkerResponse | WorkerInterrupt;
|
|
126
153
|
|
|
@@ -133,6 +160,8 @@ export const INTERRUPT_KINDS = Object.freeze(
|
|
|
133
160
|
"await",
|
|
134
161
|
"finish",
|
|
135
162
|
"add_context",
|
|
163
|
+
"ledger_claims",
|
|
164
|
+
"memory",
|
|
136
165
|
]),
|
|
137
166
|
);
|
|
138
167
|
|
|
@@ -158,6 +187,39 @@ export function isWorkerMessage(msg: unknown): msg is WorkerMessage {
|
|
|
158
187
|
return isWorkerResponse(msg) || isInterrupt(msg);
|
|
159
188
|
}
|
|
160
189
|
|
|
190
|
+
/** Unsettled Task still in the worker, optionally bound to a REPL variable. */
|
|
191
|
+
export interface PendingTaskInfo {
|
|
192
|
+
readonly var: string | null;
|
|
193
|
+
readonly kind: string;
|
|
194
|
+
readonly label: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const EMPTY_PENDING: readonly PendingTaskInfo[] = Object.freeze([]);
|
|
198
|
+
|
|
199
|
+
function isPendingTaskInfo(value: unknown): value is PendingTaskInfo {
|
|
200
|
+
if (!isRecord(value)) return false;
|
|
201
|
+
const bound = value["var"];
|
|
202
|
+
return (typeof bound === "string" || bound === null)
|
|
203
|
+
&& typeof value["kind"] === "string"
|
|
204
|
+
&& typeof value["label"] === "string";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Narrow a worker `pending_tasks` payload; drop malformed entries. */
|
|
208
|
+
export function parsePendingTasks(value: unknown): readonly PendingTaskInfo[] {
|
|
209
|
+
if (!Array.isArray(value)) return EMPTY_PENDING;
|
|
210
|
+
const out = new Array<PendingTaskInfo>(value.length);
|
|
211
|
+
let n = 0;
|
|
212
|
+
for (let i = 0; i < value.length; i++) {
|
|
213
|
+
const item: unknown = value[i];
|
|
214
|
+
if (isPendingTaskInfo(item)) {
|
|
215
|
+
out[n] = item;
|
|
216
|
+
n += 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
out.length = n;
|
|
220
|
+
return n === 0 ? EMPTY_PENDING : Object.freeze(out);
|
|
221
|
+
}
|
|
222
|
+
|
|
161
223
|
/** Result of a single `repl` block execution, surfaced to the engine/tool. */
|
|
162
224
|
export interface ReplResult {
|
|
163
225
|
readonly stdout: string;
|
|
@@ -168,4 +230,6 @@ export interface ReplResult {
|
|
|
168
230
|
readonly executionTimeMs: number;
|
|
169
231
|
/** User-created variable names after this exec (builtins/context filtered out). */
|
|
170
232
|
readonly varNames: readonly string[];
|
|
233
|
+
/** Unsettled Task handles still in the worker after this exec. */
|
|
234
|
+
readonly pendingTasks: readonly PendingTaskInfo[];
|
|
171
235
|
}
|
|
Binary file
|
|
Binary file
|