@hicaru/pi-rlm 0.3.6 → 0.3.8
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/handlers/llm-query.ts +73 -17
- package/src/bridge/handlers/rlm-query.ts +130 -8
- package/src/bridge/handlers/types.ts +12 -0
- package/src/config/defaults.ts +17 -0
- package/src/config/settings.ts +45 -2
- 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 +171 -5
- package/src/core/ledger.ts +451 -0
- package/src/core/memory.ts +577 -0
- package/src/core/model-registry.ts +88 -0
- package/src/core/types.ts +44 -3
- package/src/index.ts +60 -9
- package/src/mode/rlm-mode.ts +47 -9
- package/src/prompts/glossary.ts +139 -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-result.ts +54 -10
- package/src/tool/repl-tool.ts +43 -9
- package/src/ui/status.ts +4 -1
- package/src/util/concurrency.ts +47 -0
package/src/core/types.ts
CHANGED
|
@@ -15,9 +15,10 @@ export interface RlmConfig {
|
|
|
15
15
|
readonly maxDepth: number;
|
|
16
16
|
/** Max turns before the engine must finalize. */
|
|
17
17
|
readonly maxIterations: number;
|
|
18
|
-
/** Per-`repl`-block wall-clock timeout inside the worker (seconds).
|
|
18
|
+
/** Per-`repl`-block wall-clock timeout inside the worker (seconds).
|
|
19
|
+
* v5 doctrine: content limits are the token budget's job — this is a HANG backstop only. */
|
|
19
20
|
readonly execTimeoutS: number;
|
|
20
|
-
/** Parent-side watchdog per sandbox request (ms). */
|
|
21
|
+
/** Parent-side watchdog per sandbox request (ms). Hang backstop (see execTimeoutS). */
|
|
21
22
|
readonly requestTimeoutMs: number;
|
|
22
23
|
/** Concurrency pool for *_batched sub-calls. */
|
|
23
24
|
readonly maxConcurrentSubcalls: number;
|
|
@@ -28,7 +29,10 @@ export interface RlmConfig {
|
|
|
28
29
|
readonly maxPromptChars: number;
|
|
29
30
|
/** Max wall-clock ms across the whole tree before the engine stops (undefined = no cap). */
|
|
30
31
|
readonly maxTimeoutMs?: number;
|
|
31
|
-
/** Max total input+output tokens across the whole tree before the engine stops (undefined = no cap).
|
|
32
|
+
/** Max total input+output tokens across the whole tree before the engine stops (undefined = no cap).
|
|
33
|
+
* ⚠ HARD ABORT (audit H8): when set, exceeding this throws a LimitError mid-run and the run
|
|
34
|
+
* ends with its best partial — NO wrap-up, NO continuation. The graceful path is the v5
|
|
35
|
+
* token budget (`enableTokenBudget`); leave this unset unless a hard tree-wide stop is wanted. */
|
|
32
36
|
readonly maxTokens?: number;
|
|
33
37
|
/** Max consecutive error turns before the engine stops (undefined = no cap). */
|
|
34
38
|
readonly maxErrors?: number;
|
|
@@ -61,6 +65,37 @@ export interface RlmConfig {
|
|
|
61
65
|
readonly subSystemPrompt?: string;
|
|
62
66
|
/** Sampling for sub-LLM (worker) calls. */
|
|
63
67
|
readonly subSampling: Readonly<Sampling>;
|
|
68
|
+
/** v5 token budget: cap = budgetShare × contextWindow, clamped by budgetTaskCap. When on,
|
|
69
|
+
* the budget is the PRIMARY run-length control (soft wrap-up → continuation chain). */
|
|
70
|
+
readonly enableTokenBudget: boolean;
|
|
71
|
+
/** Fraction of the model's context window that forms one run's token cap. */
|
|
72
|
+
readonly budgetShare: number;
|
|
73
|
+
/** Soft wrap-up fires at this fraction of the cap (one wrap-up turn). */
|
|
74
|
+
readonly budgetSoftFrac: number;
|
|
75
|
+
/** Absolute single-run ceiling; 0 = no clamp beyond the share. */
|
|
76
|
+
readonly budgetTaskCap: number;
|
|
77
|
+
/** Max continuation runs after a hard stop (chain ≤ 1 + this). */
|
|
78
|
+
readonly budgetMaxContinuations: number;
|
|
79
|
+
/** Char budget for the deterministic continuation handoff. */
|
|
80
|
+
readonly budgetHandoffChars: number;
|
|
81
|
+
/** v5 TaskLedger blackboard: claim coalescing + ancestor-echo reject + `[ledger]` injection. */
|
|
82
|
+
readonly enableLedger: boolean;
|
|
83
|
+
/** Real rlm spawns allowed before extra rlm_query demotes to llm_query (0 = never). */
|
|
84
|
+
readonly rlmBudget: number;
|
|
85
|
+
/** v5 durable memory: L1 episode replay + L2 BM25 notes under `<root>/.rlm/memory`. */
|
|
86
|
+
readonly enableMemory: boolean;
|
|
87
|
+
/** Char budget for the `[memory]` injection = tokens × 4. */
|
|
88
|
+
readonly injectNoteTokens: number;
|
|
89
|
+
/** Pending episodes per L2 consolidation batch (0 = never auto-consolidate). */
|
|
90
|
+
readonly evolveEvery: number;
|
|
91
|
+
/** Override the memory dir. `null` (default) = `<root>/.rlm/memory` — this field only
|
|
92
|
+
* RELOCATES the store; the on/off switch is `enableMemory` (audit M3). */
|
|
93
|
+
readonly memoryDir: string | null;
|
|
94
|
+
/** v5: per-provider concurrent-request caps (e.g. `{ zai: 4 }`). Caps only lower limits. */
|
|
95
|
+
readonly providerMaxConcurrent?: Readonly<Record<string, number>>;
|
|
96
|
+
/** v5 doctrine: "delegation" = child engines get llm/memory/ledger only (no repo retrieval);
|
|
97
|
+
* "legacy" keeps today's full child surface as a one-flip rollback. */
|
|
98
|
+
readonly childSurface: "delegation" | "legacy";
|
|
64
99
|
}
|
|
65
100
|
|
|
66
101
|
/** Input to a (headless) RLM run. */
|
|
@@ -75,6 +110,12 @@ export interface RlmInput {
|
|
|
75
110
|
readonly parentNodeId?: string;
|
|
76
111
|
/** Remaining timeout for this subtree (set by parent from its LimitGuard). */
|
|
77
112
|
readonly remainingTimeoutMs?: number;
|
|
113
|
+
/** v5: budget for this run. Set only by the engine itself when chaining a continuation —
|
|
114
|
+
* a fresh budget is resolved from config when omitted. */
|
|
115
|
+
readonly budget?: import("./budget.ts").TokenBudget;
|
|
116
|
+
/** v5: the shared TaskLedger blackboard. Children inherit the parent's instance —
|
|
117
|
+
* set by childRun (the one child-RlmInput construction path); a fresh run gets a new one. */
|
|
118
|
+
readonly ledger?: import("./ledger.ts").TaskLedger;
|
|
78
119
|
}
|
|
79
120
|
|
|
80
121
|
/** Result of a completed RLM run. */
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { Markdown } from "@earendil-works/pi-tui";
|
|
5
5
|
import { registerRlmCommand } from "./commands/rlm.ts";
|
|
6
6
|
import { registerRlmConfigCommand } from "./commands/rlm-config.ts";
|
|
7
|
+
import type { RlmConfig } from "./core/types.ts";
|
|
7
8
|
import { createRlmTool } from "./tool/rlm-tool.ts";
|
|
8
9
|
import { createReplTool } from "./tool/repl-tool.ts";
|
|
9
10
|
import { loadSettings, mergeConfig, resolveModelId } from "./config/settings.ts";
|
|
@@ -12,9 +13,12 @@ import { cheapestModel } from "./mode/llm-model.ts";
|
|
|
12
13
|
import { postRlmGuide } from "./ui/intro.ts";
|
|
13
14
|
import { setRlmModeStatus } from "./ui/status.ts";
|
|
14
15
|
import { markdownTheme } from "./ui/theme-adapter.ts";
|
|
16
|
+
import { SANDBOX_WATCHDOG_HEARTBEAT_MS } from "./sandbox/sandbox.ts";
|
|
15
17
|
import { SandboxManager } from "./sandbox/sandbox-manager.ts";
|
|
16
|
-
import {
|
|
18
|
+
import { buildSessionGates, type SubcallGates } from "./util/concurrency.ts";
|
|
17
19
|
import { BackgroundTasks } from "./tool/background-tasks.ts";
|
|
20
|
+
import { MemoryStore } from "./core/memory.ts";
|
|
21
|
+
import { modelComplete } from "./bridge/model.ts";
|
|
18
22
|
import { resolve } from "node:path";
|
|
19
23
|
import { resolveSource } from "./context/resolve.ts";
|
|
20
24
|
import { formatContextListing } from "./context/listing.ts";
|
|
@@ -38,8 +42,6 @@ export {
|
|
|
38
42
|
processRlmDepth,
|
|
39
43
|
} from "./mode/subagent.ts";
|
|
40
44
|
|
|
41
|
-
/** How often to keep the parent sandbox's request watchdog alive during detached work. */
|
|
42
|
-
const WATCHDOG_HEARTBEAT_MS = 30_000;
|
|
43
45
|
/** Soft token guard — cap bulk tool stdout; do NOT hard-block read/grep/bash readers. */
|
|
44
46
|
const CAPPED_RESULT_TOOLS = Object.freeze(new Set(["bash", "find", "ls", "read", "grep"]));
|
|
45
47
|
|
|
@@ -62,7 +64,21 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
62
64
|
|
|
63
65
|
// Init synchronously with defaults — ensures commands/tools/handlers register before session_start
|
|
64
66
|
const config = mergeConfig({});
|
|
65
|
-
|
|
67
|
+
// v5 durable memory: one store per session under <cwd>/.rlm/memory (L1 replay + L2 notes).
|
|
68
|
+
// NOTE (audit M4): this store IS shared by both composition roots, but the TaskLedger is
|
|
69
|
+
// NOT — the native repl() session and each headless rlm run each keep their own blackboard
|
|
70
|
+
// (v5 parity: per-run ledger). Claims/coalescing reset at that boundary, by design.
|
|
71
|
+
// The consolidation LLM + real workspace root are attached in session_start (setLlm/setRoot).
|
|
72
|
+
const memory = new MemoryStore(
|
|
73
|
+
process.cwd(),
|
|
74
|
+
{
|
|
75
|
+
dir: config.memoryDir ?? undefined,
|
|
76
|
+
injectNoteTokens: config.injectNoteTokens,
|
|
77
|
+
evolveEvery: config.evolveEvery,
|
|
78
|
+
},
|
|
79
|
+
config.enableMemory,
|
|
80
|
+
);
|
|
81
|
+
const controller = new RlmController(config, memory);
|
|
66
82
|
let onSandboxDiscardExtra: (() => void) | undefined;
|
|
67
83
|
const sandboxManager = new SandboxManager({
|
|
68
84
|
execTimeoutS: config.execTimeoutS,
|
|
@@ -75,9 +91,8 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
75
91
|
awaitTimeoutS: Math.round(config.requestTimeoutMs / 1000),
|
|
76
92
|
onSandboxDiscarded: () => { onSandboxDiscardExtra?.(); },
|
|
77
93
|
});
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
const gates = createSubcallGates(config.maxConcurrentSubcalls, config.maxConcurrentChildren);
|
|
94
|
+
// v5: sub-call admission is built per session (see session_start) so provider concurrency
|
|
95
|
+
// caps resolve against the models actually in use.
|
|
81
96
|
const background = new BackgroundTasks({
|
|
82
97
|
maxTimeoutMs: config.maxTimeoutMs,
|
|
83
98
|
maxTokens: config.maxTokens,
|
|
@@ -88,7 +103,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
88
103
|
// with it. Keep it alive while detached work is genuinely in flight.
|
|
89
104
|
const watchdogHeartbeat = setInterval(() => {
|
|
90
105
|
if (background.pending > 0) sandboxManager.refreshWatchdog();
|
|
91
|
-
},
|
|
106
|
+
}, SANDBOX_WATCHDOG_HEARTBEAT_MS);
|
|
92
107
|
watchdogHeartbeat.unref();
|
|
93
108
|
|
|
94
109
|
/** Memoised cwd seed — one resolveSource(pathPrefix:"") per session. */
|
|
@@ -198,6 +213,40 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
198
213
|
const llmModel = controller.llmModel ?? cheapestModel(ctx.modelRegistry) ?? ctx.model;
|
|
199
214
|
const model = ctx.model;
|
|
200
215
|
if (llmModel && model) {
|
|
216
|
+
// Consolidation runs on the cheap worker model through the single completion entry point;
|
|
217
|
+
// the workspace root is only known once the session starts.
|
|
218
|
+
const consolidateModel = llmModel;
|
|
219
|
+
memory.setLlm((prompt) =>
|
|
220
|
+
modelComplete([{ role: "user", content: prompt }], { model: consolidateModel, registry: ctx.modelRegistry })
|
|
221
|
+
.then((r) => r.text));
|
|
222
|
+
memory.setRoot(ctx.cwd ?? process.cwd());
|
|
223
|
+
// v5 provider caps (audit C1/C6): ONE resolver shared by both composition roots — the
|
|
224
|
+
// repl() tool and RlmController.start admit through the same pool, each gate capped
|
|
225
|
+
// against the model that actually runs on it (leaves = worker, children = smart).
|
|
226
|
+
// Memoized on (config, providers) so /rlm-config changes apply without a restart.
|
|
227
|
+
let gatesMemo:
|
|
228
|
+
| { readonly config: RlmConfig; readonly smart: string; readonly worker: string; readonly gates: SubcallGates }
|
|
229
|
+
| undefined;
|
|
230
|
+
const resolveSessionGates = (): SubcallGates => {
|
|
231
|
+
const smart = model;
|
|
232
|
+
const worker = controller.llmModel ?? cheapestModel(ctx.modelRegistry) ?? model;
|
|
233
|
+
const workerProvider = worker.provider;
|
|
234
|
+
if (
|
|
235
|
+
gatesMemo === undefined ||
|
|
236
|
+
gatesMemo.config !== controller.config ||
|
|
237
|
+
gatesMemo.smart !== smart.provider ||
|
|
238
|
+
gatesMemo.worker !== workerProvider
|
|
239
|
+
) {
|
|
240
|
+
gatesMemo = {
|
|
241
|
+
config: controller.config,
|
|
242
|
+
smart: smart.provider,
|
|
243
|
+
worker: workerProvider,
|
|
244
|
+
gates: buildSessionGates(controller.config, smart.provider, workerProvider),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return gatesMemo.gates;
|
|
248
|
+
};
|
|
249
|
+
controller.setSessionGates(resolveSessionGates);
|
|
201
250
|
try {
|
|
202
251
|
pi.registerTool(createReplTool({
|
|
203
252
|
sandboxManager,
|
|
@@ -207,8 +256,10 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
207
256
|
getLlmModel: () => controller.resolveModels(ctx)?.llm,
|
|
208
257
|
registry: ctx.modelRegistry,
|
|
209
258
|
getConfig: () => controller.config,
|
|
210
|
-
gates,
|
|
259
|
+
gates: resolveSessionGates(),
|
|
260
|
+
resolveGates: resolveSessionGates,
|
|
211
261
|
background,
|
|
262
|
+
memory,
|
|
212
263
|
registerDiscardHook: (reset) => { onSandboxDiscardExtra = reset; },
|
|
213
264
|
registerContextBundle: (bundle) => {
|
|
214
265
|
contextBundleRef = bundle;
|
package/src/mode/rlm-mode.ts
CHANGED
|
@@ -9,13 +9,16 @@
|
|
|
9
9
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
10
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { modelRef, resolveModelId, saveSettings } from "../config/settings.ts";
|
|
12
|
-
import { createEngine } from "../core/engine.ts";
|
|
12
|
+
import { createEngine, type EngineDeps } from "../core/engine.ts";
|
|
13
13
|
import { limitsFromConfig } from "../core/limits.ts";
|
|
14
14
|
import type { RlmConfig, RlmResult } from "../core/types.ts";
|
|
15
15
|
import { resolveSource } from "../context/resolve.ts";
|
|
16
16
|
import { RlmEmitter } from "../tool/rlm-events.ts";
|
|
17
17
|
import { formatError } from "../util/errors.ts";
|
|
18
18
|
import { cheapestModel } from "./llm-model.ts";
|
|
19
|
+
import type { MemoryStore } from "../core/memory.ts";
|
|
20
|
+
import type { RunRlm } from "../core/types.ts";
|
|
21
|
+
import type { SubcallGates } from "../util/concurrency.ts";
|
|
19
22
|
|
|
20
23
|
export interface RunHandle {
|
|
21
24
|
readonly abort: () => void;
|
|
@@ -33,8 +36,19 @@ export class RlmController {
|
|
|
33
36
|
/** Set by applyLlmSelection when the user explicitly picks "cheapest (auto)". */
|
|
34
37
|
explicitClearPin = false;
|
|
35
38
|
private active: AbortController | null = null;
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
/** v5: session admission gates (provider-capped), shared with the repl() tool — set at
|
|
40
|
+
* session_start so BOTH composition roots admit through one pool (audit C1). */
|
|
41
|
+
private sessionGates: (() => SubcallGates) | undefined;
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
public config: RlmConfig,
|
|
45
|
+
/** v5 durable memory — shared with the repl tool so child runs replay/persist too. */
|
|
46
|
+
public readonly memory?: MemoryStore,
|
|
47
|
+
) {}
|
|
48
|
+
|
|
49
|
+
setSessionGates(getGates: () => SubcallGates): void {
|
|
50
|
+
this.sessionGates = getGates;
|
|
51
|
+
}
|
|
38
52
|
|
|
39
53
|
get enabled(): boolean {
|
|
40
54
|
return this.config.enabled;
|
|
@@ -83,6 +97,33 @@ export class RlmController {
|
|
|
83
97
|
return { model, llm };
|
|
84
98
|
}
|
|
85
99
|
|
|
100
|
+
/** Test seam (audit R7): intercept the exact object `createEngine` receives. */
|
|
101
|
+
protected spawnEngine(deps: EngineDeps): RunRlm {
|
|
102
|
+
return createEngine(deps);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The ONE engine construction path for this controller (DRY #6 — a second path that
|
|
106
|
+
* forgets to grow is exactly how issue #4 and audit C1 happened). Protected so tests can
|
|
107
|
+
* subclass and assert the wiring without touching the network. */
|
|
108
|
+
protected buildEngine(args: {
|
|
109
|
+
readonly ctx: ExtensionContext;
|
|
110
|
+
readonly models: { readonly model: Model<Api>; readonly llm: Model<Api> };
|
|
111
|
+
readonly signal: AbortSignal;
|
|
112
|
+
readonly emitter: RlmEmitter;
|
|
113
|
+
}): RunRlm {
|
|
114
|
+
return this.spawnEngine({
|
|
115
|
+
model: args.models.model,
|
|
116
|
+
llmModel: args.models.llm,
|
|
117
|
+
registry: args.ctx.modelRegistry,
|
|
118
|
+
config: this.config,
|
|
119
|
+
signal: args.signal,
|
|
120
|
+
emitter: args.emitter,
|
|
121
|
+
limits: limitsFromConfig(this.config),
|
|
122
|
+
memory: this.memory,
|
|
123
|
+
gates: this.sessionGates?.(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
86
127
|
start(ctx: ExtensionContext, input: StartInput, emitter?: RlmEmitter): RunHandle {
|
|
87
128
|
const models = this.resolveModels(ctx);
|
|
88
129
|
if (!models) throw new Error("no model with configured auth is available");
|
|
@@ -102,14 +143,11 @@ export class RlmController {
|
|
|
102
143
|
? result.value.payload
|
|
103
144
|
: formatError(`failed to pack repository — ${result.error}`);
|
|
104
145
|
}
|
|
105
|
-
const engine =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
registry: ctx.modelRegistry,
|
|
109
|
-
config: this.config,
|
|
146
|
+
const engine = this.buildEngine({
|
|
147
|
+
ctx,
|
|
148
|
+
models,
|
|
110
149
|
signal: abortController.signal,
|
|
111
150
|
emitter: emitter ?? new RlmEmitter(),
|
|
112
|
-
limits: limitsFromConfig(this.config),
|
|
113
151
|
});
|
|
114
152
|
return await engine({ rootPrompt: input.rootPrompt, context: contextValue, depth: 0 });
|
|
115
153
|
})().finally(() => {
|
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,29 @@ 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
|
+
"- `memory.query(q) -> str` / `memory.add(text, paths=…, tags=…)`: durable notes under `.rlm/`",
|
|
361
|
+
" that survive across sessions. Query before re-studying a known area; add concise findings",
|
|
362
|
+
" (facts, locations, decisions) — never secrets or API keys (notes persist on disk).",
|
|
363
|
+
"- `list_claims()`: the live `[ledger]` table of inflight/done agent work.",
|
|
282
364
|
'- `answer`: a dict initialized to {"content": "", "ready": False}. To submit your final answer,',
|
|
283
365
|
' set `answer["content"]` to the answer text and `answer["ready"] = True`.',
|
|
284
366
|
' **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>",
|