@hicaru/pi-rlm 0.2.2 → 0.3.0
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 +20 -16
- package/README.ru.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +22 -19
- package/src/bridge/add-context.ts +322 -0
- package/src/bridge/subcall-handlers.ts +1 -1
- package/src/config/defaults.ts +2 -1
- package/src/config/settings.ts +5 -2
- package/src/context/anydoc.ts +67 -0
- package/src/context/listing.ts +70 -0
- package/src/context/md-cache.ts +112 -0
- package/src/context/merge.ts +97 -0
- package/src/context/namespace.ts +180 -0
- package/src/context/resolve.ts +122 -0
- package/src/context/source-dir.ts +166 -0
- package/src/context/source-doc.ts +71 -0
- package/src/context/source-git.ts +51 -0
- package/src/context/source-text.ts +45 -0
- package/src/context/types.ts +88 -0
- package/src/context/walk.ts +250 -0
- package/src/core/engine.ts +15 -19
- package/src/core/types.ts +7 -2
- package/src/index.ts +69 -42
- package/src/mode/rlm-mode.ts +5 -4
- package/src/prompts/glossary.ts +31 -28
- package/src/prompts/native.ts +4 -4
- package/src/prompts/system.ts +2 -2
- package/src/sandbox/context-file.ts +4 -4
- package/src/sandbox/interrupts.ts +25 -10
- package/src/sandbox/protocol.ts +13 -7
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +1 -1
- package/src/sandbox/py/retrieval.py +1 -1
- package/src/sandbox/py/tasks.py +17 -4
- package/src/sandbox/py/worker.py +68 -48
- package/src/sandbox/sandbox-manager.ts +18 -16
- package/src/sandbox/sandbox.ts +1 -1
- package/src/text/tokens.ts +3 -3
- package/src/tool/repl-details.ts +1 -1
- package/src/tool/repl-tool.ts +31 -19
- package/src/tool/rlm-tool.ts +1 -1
- package/src/ui/config-panel.ts +8 -4
- package/src/bridge/library.ts +0 -190
- package/src/context/library-context.ts +0 -339
- package/src/context/repomix-context.ts +0 -204
package/src/core/engine.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import type { Api, Model, Usage } from "@earendil-works/pi-ai";
|
|
11
11
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { buildAddContextHandler } from "../bridge/add-context.ts";
|
|
13
|
+
import { mergeIntoContext } from "../context/merge.ts";
|
|
14
14
|
import {
|
|
15
15
|
createSubcallHandlers,
|
|
16
16
|
type Invocation,
|
|
@@ -30,7 +30,6 @@ import { appendUserMessage } from "./history.ts";
|
|
|
30
30
|
import { runTurn } from "./iteration.ts";
|
|
31
31
|
import { type Limits, LimitError, LimitGuard } from "./limits.ts";
|
|
32
32
|
import type { RlmConfig, RlmInput, RlmResult, RunRlm, Sampling } from "./types.ts";
|
|
33
|
-
import { serializeForSandbox, type ContextBundle } from "../context/repomix-context.ts";
|
|
34
33
|
import { formatError } from "../util/errors.ts";
|
|
35
34
|
import { createSubcallGates, type SubcallGates } from "../util/concurrency.ts";
|
|
36
35
|
|
|
@@ -128,7 +127,7 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
128
127
|
getConfig: () => deps.config,
|
|
129
128
|
signal: deps.signal,
|
|
130
129
|
runChild: run,
|
|
131
|
-
// Read lazily: a
|
|
130
|
+
// Read lazily: a source added on turn 3 must reach a child spawned on turn 4. Safe
|
|
132
131
|
// despite being wired before liveContext is assigned — children can only spawn from an
|
|
133
132
|
// interrupt during runTurn, which is strictly after loadContext below.
|
|
134
133
|
getChildContext: () => liveContext,
|
|
@@ -153,19 +152,19 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
153
152
|
};
|
|
154
153
|
let sandbox: PythonSandbox | undefined;
|
|
155
154
|
/**
|
|
156
|
-
* This run's live context:
|
|
157
|
-
* it, so it must grow when
|
|
155
|
+
* This run's live context: whatever was seeded plus every source added so far. Children
|
|
156
|
+
* inherit it, so it must grow when add_context appends (see the handler's onLoaded below).
|
|
158
157
|
*
|
|
159
158
|
* Run-scoped on purpose — recursion means N of these are live at once, and a module-level
|
|
160
159
|
* "current context" would hand a depth-3 child its cousin's world.
|
|
161
160
|
*/
|
|
162
|
-
let liveContext: unknown =
|
|
161
|
+
let liveContext: unknown = [];
|
|
163
162
|
/**
|
|
164
163
|
* This run's hold on the serialized context file. Kept for the whole run so every child that
|
|
165
164
|
* inherits the same payload reuses one file instead of re-serializing the repository.
|
|
166
165
|
*/
|
|
167
166
|
let contextPin: PinnedContext | undefined;
|
|
168
|
-
/** Re-pin after the payload changes identity (
|
|
167
|
+
/** Re-pin after the payload changes identity (mergeIntoContext returns a new array). */
|
|
169
168
|
const repinLiveContext = async (): Promise<void> => {
|
|
170
169
|
const previous = contextPin;
|
|
171
170
|
contextPin = await pinContext(liveContext);
|
|
@@ -188,12 +187,12 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
188
187
|
orchestrator: deps.config.orchestrator,
|
|
189
188
|
recursion: input.depth + 1 < deps.config.maxDepth,
|
|
190
189
|
maxPromptChars: deps.config.maxPromptChars,
|
|
191
|
-
|
|
190
|
+
contextLoader: deps.config.contextLoader,
|
|
192
191
|
child: input.depth > 0,
|
|
193
192
|
});
|
|
194
193
|
|
|
195
|
-
const
|
|
196
|
-
?
|
|
194
|
+
const contextHandlers = deps.config.contextLoader
|
|
195
|
+
? buildAddContextHandler({
|
|
197
196
|
cwd: runCwd,
|
|
198
197
|
emitter,
|
|
199
198
|
parentId: selfReportId,
|
|
@@ -201,8 +200,8 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
201
200
|
getContext: () => liveContext,
|
|
202
201
|
onLoaded: async (payload) => {
|
|
203
202
|
// The accumulator is what children inherit, which is what makes inheritance
|
|
204
|
-
// transitive — grandchildren see the
|
|
205
|
-
liveContext =
|
|
203
|
+
// transitive — grandchildren see the source too.
|
|
204
|
+
liveContext = mergeIntoContext(liveContext, payload);
|
|
206
205
|
await repinLiveContext();
|
|
207
206
|
},
|
|
208
207
|
}).handlers
|
|
@@ -217,17 +216,14 @@ export function createEngine(deps: EngineDeps): RunRlm {
|
|
|
217
216
|
initTimeoutMs: deps.config.sandboxInitTimeoutMs,
|
|
218
217
|
maxPromptChars: deps.config.maxPromptChars,
|
|
219
218
|
awaitTimeoutS: Math.round(deps.config.requestTimeoutMs / 1000),
|
|
220
|
-
handlers: { ...subcalls, ...
|
|
219
|
+
handlers: { ...subcalls, ...contextHandlers },
|
|
221
220
|
});
|
|
222
221
|
|
|
223
222
|
let history: ChatMsg[] = [{ role: "system", content: system }];
|
|
224
223
|
let pendingReplOutputs: string | undefined;
|
|
225
224
|
|
|
226
|
-
// Context
|
|
227
|
-
liveContext =
|
|
228
|
-
typeof input.context === "object" && input.context !== null && "files" in input.context
|
|
229
|
-
? serializeForSandbox(input.context as ContextBundle)
|
|
230
|
-
: input.context;
|
|
225
|
+
// Context is already a sandbox-ready list (or a raw string for text children).
|
|
226
|
+
liveContext = input.context ?? [];
|
|
231
227
|
contextPin = await pinContext(liveContext);
|
|
232
228
|
await sandbox.loadContextPinned(contextPin);
|
|
233
229
|
for (let i = 0; i < deps.config.maxIterations; i++) {
|
package/src/core/types.ts
CHANGED
|
@@ -42,8 +42,13 @@ export interface RlmConfig {
|
|
|
42
42
|
readonly python: string;
|
|
43
43
|
/** Worker startup wait before treating sandbox init as failed (ms). */
|
|
44
44
|
readonly sandboxInitTimeoutMs: number;
|
|
45
|
-
/** Enable the
|
|
46
|
-
readonly
|
|
45
|
+
/** Enable the add_context() REPL scaffold (external dirs/files/git repos/documents into context). */
|
|
46
|
+
readonly contextLoader: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* When true, the first repl() call seeds `context` with the working directory
|
|
49
|
+
* (un-prefixed paths). When false, context stays empty until add_context is called.
|
|
50
|
+
*/
|
|
51
|
+
readonly autoSeedCwd: boolean;
|
|
47
52
|
/** ThinkingLevel for the root smart model (set via /rlm-config). */
|
|
48
53
|
readonly smartReasoning?: ThinkingLevel;
|
|
49
54
|
/** Output token cap + temperature for the root smart model per turn.
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,10 @@ import { markdownTheme } from "./ui/theme-adapter.ts";
|
|
|
15
15
|
import { SandboxManager } from "./sandbox/sandbox-manager.ts";
|
|
16
16
|
import { createSubcallGates } from "./util/concurrency.ts";
|
|
17
17
|
import { BackgroundTasks } from "./tool/background-tasks.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { resolve } from "node:path";
|
|
19
|
+
import { resolveSource } from "./context/resolve.ts";
|
|
20
|
+
import { formatContextListing } from "./context/listing.ts";
|
|
21
|
+
import type { AddContextHandlerBundle } from "./bridge/add-context.ts";
|
|
19
22
|
import { buildNativeSystemPrompt, NATIVE_TURN_REMINDER } from "./prompts/native.ts";
|
|
20
23
|
import { bashCommandFromInput, isFileReadingCommand, capToolResultText, BASH_BLOCK_REASON } from "./mode/native-guards.ts";
|
|
21
24
|
import { errorMessage } from "./util/errors.ts";
|
|
@@ -57,22 +60,37 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
57
60
|
}, WATCHDOG_HEARTBEAT_MS);
|
|
58
61
|
watchdogHeartbeat.unref();
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
/** Memoised cwd seed — one resolveSource(pathPrefix:"") per session. */
|
|
64
|
+
let seedPromise: Promise<void> | undefined;
|
|
65
|
+
let cwdSeeded = false;
|
|
66
|
+
/** Live add_context bundle — seed plants the "" sentinel here so add_context(".") is a no-op. */
|
|
67
|
+
let contextBundleRef: AddContextHandlerBundle | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Payload identity last injected into the context hook. Re-inject only when the payload
|
|
70
|
+
* reference changes (seed, add_context, reset) — not every turn. Keeps the root window small.
|
|
71
|
+
*/
|
|
72
|
+
let listingPayloadRef: unknown = undefined;
|
|
73
|
+
let listingInjected = false;
|
|
74
|
+
const seedContext = async (cwd: string): Promise<void> => {
|
|
75
|
+
if (cwdSeeded) return;
|
|
76
|
+
if (!controller.config.autoSeedCwd) {
|
|
77
|
+
cwdSeeded = true;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
seedPromise ??= resolveSource(cwd, { cwd, pathPrefix: "" })
|
|
65
81
|
.then((result) => {
|
|
82
|
+
// Sticky either way: a failed seed must not re-walk the whole repo on every repl().
|
|
83
|
+
cwdSeeded = true;
|
|
66
84
|
if (!result.ok) {
|
|
67
|
-
console.warn(`[rlm]
|
|
68
|
-
return
|
|
85
|
+
console.warn(`[rlm] context seed failed: ${result.error}`);
|
|
86
|
+
return;
|
|
69
87
|
}
|
|
70
|
-
sandboxManager.contextPayload =
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
sandboxManager.contextPayload = result.value.payload;
|
|
89
|
+
// Register the cwd seed with the bridge so add_context(".") cannot double the tree.
|
|
90
|
+
contextBundleRef?.markSeededCwd(resolve(cwd));
|
|
73
91
|
})
|
|
74
|
-
.finally(() => {
|
|
75
|
-
|
|
92
|
+
.finally(() => { seedPromise = undefined; });
|
|
93
|
+
await seedPromise;
|
|
76
94
|
};
|
|
77
95
|
|
|
78
96
|
// Load persisted settings async — applied before session_start handler reads controller state
|
|
@@ -152,9 +170,16 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
152
170
|
gates,
|
|
153
171
|
background,
|
|
154
172
|
registerDiscardHook: (reset) => { onSandboxDiscardExtra = reset; },
|
|
173
|
+
registerContextBundle: (bundle) => {
|
|
174
|
+
contextBundleRef = bundle;
|
|
175
|
+
// Tool re-registers each session; re-plant sentinel if seed already landed.
|
|
176
|
+
if (cwdSeeded && Array.isArray(sandboxManager.contextPayload)
|
|
177
|
+
&& sandboxManager.contextPayload.length > 0) {
|
|
178
|
+
bundle.markSeededCwd(resolve(ctx.cwd ?? process.cwd()));
|
|
179
|
+
}
|
|
180
|
+
},
|
|
155
181
|
ensureContext: async () => {
|
|
156
|
-
|
|
157
|
-
if (contextText === undefined) throw new Error("repository context could not be loaded into RLM sandbox");
|
|
182
|
+
await seedContext(ctx.cwd ?? process.cwd());
|
|
158
183
|
},
|
|
159
184
|
}));
|
|
160
185
|
} catch (err) {
|
|
@@ -184,9 +209,10 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
184
209
|
return { systemPrompt: event.systemPrompt + "\n\n" + buildNativeSystemPrompt() };
|
|
185
210
|
});
|
|
186
211
|
|
|
187
|
-
// ── Context injection:
|
|
188
|
-
|
|
189
|
-
|
|
212
|
+
// ── Context injection: listing of whatever is currently loaded ──
|
|
213
|
+
// Re-inject only when the payload identity changes (seed / add_context), not every turn —
|
|
214
|
+
// the listing can be up to 200 file lines and the plugin exists to shrink the root window.
|
|
215
|
+
pi.on("context", async (event) => {
|
|
190
216
|
const filtered = event.messages.filter(
|
|
191
217
|
(message) =>
|
|
192
218
|
!(message.role === "custom" && message.customType === "rlm-intro")
|
|
@@ -196,25 +222,24 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
196
222
|
|
|
197
223
|
type PiMessage = (typeof filtered)[number];
|
|
198
224
|
|
|
199
|
-
|
|
200
|
-
if (!
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
225
|
+
const payload = sandboxManager.contextPayload;
|
|
226
|
+
if (!listingInjected || payload !== listingPayloadRef) {
|
|
227
|
+
listingInjected = true;
|
|
228
|
+
listingPayloadRef = payload;
|
|
229
|
+
const listing = formatContextListing(payload);
|
|
230
|
+
const instruction = [
|
|
231
|
+
"ANALYZE with repl({code}) — read/grep are DISABLED.",
|
|
232
|
+
"Files you have loaded live in the Python REPL `context` variable (starts empty; cwd seeds on first repl()).",
|
|
233
|
+
"Locate with search()/grep_context()/outline() (free), then delegate bulk reading to",
|
|
234
|
+
"map_files()/llm_query_batched(). Use add_context(path) for external dirs/files/docs/git URLs.",
|
|
235
|
+
"If credits exhausted → report and stop.",
|
|
236
|
+
"",
|
|
237
|
+
].join("\n");
|
|
238
|
+
filtered.unshift({
|
|
239
|
+
role: "user" as const,
|
|
240
|
+
content: instruction + listing,
|
|
241
|
+
timestamp: 0,
|
|
242
|
+
} as PiMessage);
|
|
218
243
|
}
|
|
219
244
|
|
|
220
245
|
// Per-turn last-position reminder (not persisted — context hook rebuilds every request)
|
|
@@ -236,7 +261,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
236
261
|
if (BLOCKED_NATIVE_TOOLS.has(event.toolName)) {
|
|
237
262
|
return {
|
|
238
263
|
block: true,
|
|
239
|
-
reason: "RLM mode active. Use repl({code}) to read files and search the repository —
|
|
264
|
+
reason: "RLM mode active. Use repl({code}) to read files and search the repository — loaded files live in the REPL `context` variable (cwd seeds on first call). Use `edit`/`write` for file changes. If sub-LLM credits are exhausted, report to the user.",
|
|
240
265
|
};
|
|
241
266
|
}
|
|
242
267
|
const bashCommand = event.toolName === "bash" ? bashCommandFromInput(event.input) : undefined;
|
|
@@ -264,9 +289,11 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
264
289
|
clearInterval(watchdogHeartbeat);
|
|
265
290
|
background.dispose();
|
|
266
291
|
await sandboxManager.dispose();
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
292
|
+
cwdSeeded = false;
|
|
293
|
+
seedPromise = undefined;
|
|
294
|
+
contextBundleRef = undefined;
|
|
295
|
+
listingPayloadRef = undefined;
|
|
296
|
+
listingInjected = false;
|
|
297
|
+
sandboxManager.contextPayload = [];
|
|
271
298
|
});
|
|
272
299
|
}
|
package/src/mode/rlm-mode.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { modelRef, resolveModelId, saveSettings } from "../config/settings.ts";
|
|
|
12
12
|
import { createEngine } from "../core/engine.ts";
|
|
13
13
|
import { limitsFromConfig } from "../core/limits.ts";
|
|
14
14
|
import type { RlmConfig, RlmResult } from "../core/types.ts";
|
|
15
|
-
import {
|
|
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";
|
|
@@ -87,13 +87,14 @@ export class RlmController {
|
|
|
87
87
|
this.active = abortController;
|
|
88
88
|
|
|
89
89
|
const done = (async () => {
|
|
90
|
-
// Auto-
|
|
90
|
+
// Auto-seed empty/undefined context from cwd (same resolveSource path as native mode);
|
|
91
|
+
// pass explicit context through.
|
|
91
92
|
let contextValue: unknown = input.context;
|
|
92
93
|
if (contextValue === undefined || (typeof contextValue === "string" && contextValue.trim() === "")) {
|
|
93
94
|
const cwd = ctx.cwd ?? process.cwd();
|
|
94
|
-
const result = await
|
|
95
|
+
const result = await resolveSource(cwd, { cwd, pathPrefix: "", signal: abortController.signal });
|
|
95
96
|
contextValue = result.ok
|
|
96
|
-
?
|
|
97
|
+
? result.value.payload
|
|
97
98
|
: formatError(`failed to pack repository — ${result.error}`);
|
|
98
99
|
}
|
|
99
100
|
const engine = createEngine({
|
package/src/prompts/glossary.ts
CHANGED
|
@@ -82,13 +82,13 @@ export const SPAWN_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
82
82
|
*/
|
|
83
83
|
export const RECURSION_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
84
84
|
"",
|
|
85
|
-
" **What a child sees:** it inherits YOUR `context` —
|
|
86
|
-
"
|
|
87
|
-
" same paths. So send instructions, never file bodies: pasting content you already
|
|
88
|
-
" your tokens twice and buys nothing. Your prompt becomes the child's question.",
|
|
89
|
-
" Narrow its world with `rlm_query(prompt, paths=['src/auth/', '
|
|
85
|
+
" **What a child sees:** it inherits YOUR `context` — every file you have loaded, including",
|
|
86
|
+
" sources under `ctx/<id>/…` — and runs `search` / `grep_context` / `outline` / `map_files`",
|
|
87
|
+
" over the same paths. So send instructions, never file bodies: pasting content you already",
|
|
88
|
+
" share costs your tokens twice and buys nothing. Your prompt becomes the child's question.",
|
|
89
|
+
" Narrow its world with `rlm_query(prompt, paths=['src/auth/', 'ctx/x-9f3a/'])` — path PREFIXES,",
|
|
90
90
|
" not globs. Omit `paths` to hand over everything.",
|
|
91
|
-
" Inheritance is one-way:
|
|
91
|
+
" Inheritance is one-way: sources the child loads, and its whole REPL, die with it — only its",
|
|
92
92
|
" final answer string returns.",
|
|
93
93
|
" At the depth cap `rlm_query` degrades to a plain sub-LLM call with NO context, which is why",
|
|
94
94
|
" this section disappears at the last recursive depth.",
|
|
@@ -99,14 +99,17 @@ export const RECURSION_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
|
99
99
|
* than a repository the run packed for itself.
|
|
100
100
|
*/
|
|
101
101
|
export const CHILD_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
102
|
-
" You are a sub-RLM. This `context` is your parent's world —
|
|
103
|
-
"
|
|
104
|
-
" you load die with you, and only your final answer string returns
|
|
102
|
+
" You are a sub-RLM. This `context` is your parent's world — every file it has loaded (cwd",
|
|
103
|
+
" paths un-prefixed; external sources under `ctx/<id>/…`). Answer only the question above;",
|
|
104
|
+
" your REPL and anything you load die with you, and only your final answer string returns.",
|
|
105
105
|
]);
|
|
106
106
|
|
|
107
107
|
/** Why a file the user mentioned may be missing from `context`. */
|
|
108
|
-
export const CONTEXT_EXCLUSION_NOTE =
|
|
109
|
-
" NOTE:
|
|
108
|
+
export const CONTEXT_EXCLUSION_NOTE = [
|
|
109
|
+
" NOTE: `context` holds only the files you have loaded (starts empty; cwd seeds on first use).",
|
|
110
|
+
" Gitignored files and files larger than 1MB of plain text are skipped. Binary documents",
|
|
111
|
+
" (PDF, DOCX, XLSX, PPTX, CSV, …) ARE included — converted to Markdown on the way in.",
|
|
112
|
+
].join("\n");
|
|
110
113
|
|
|
111
114
|
/** The large-on-disk-file protocol (headless + native). */
|
|
112
115
|
export const LARGE_FILE_RULE_LINES: readonly string[] = Object.freeze([
|
|
@@ -200,7 +203,7 @@ export function howToRunCode(): string {
|
|
|
200
203
|
export function replGlossary(
|
|
201
204
|
kind: ContextKind,
|
|
202
205
|
recursion: boolean,
|
|
203
|
-
|
|
206
|
+
contextLoader: boolean,
|
|
204
207
|
child: boolean,
|
|
205
208
|
): string {
|
|
206
209
|
const lines = ["Available in the REPL:"];
|
|
@@ -212,10 +215,10 @@ export function replGlossary(
|
|
|
212
215
|
);
|
|
213
216
|
} else {
|
|
214
217
|
lines.push(
|
|
215
|
-
"- `context`: list[dict] —
|
|
216
|
-
" keys: `path` (
|
|
217
|
-
"
|
|
218
|
-
"
|
|
218
|
+
"- `context`: list[dict] — the files you have loaded (starts empty; cwd seeds on first use).",
|
|
219
|
+
" Each dict has keys: `path` (str), `content` (str), `tokens` (int).",
|
|
220
|
+
" Cwd paths are un-prefixed (real paths for edit/write); external sources land under",
|
|
221
|
+
" `ctx/<source_id>/…`. For large sets, chunk and delegate — never dump raw file bodies.",
|
|
219
222
|
CONTEXT_EXCLUSION_NOTE,
|
|
220
223
|
);
|
|
221
224
|
if (child) lines.push(...CHILD_CONTEXT_LINES);
|
|
@@ -240,22 +243,22 @@ export function replGlossary(
|
|
|
240
243
|
...SPAWN_GLOSSARY_LINES,
|
|
241
244
|
...DELEGATION_GLOSSARY_LINES,
|
|
242
245
|
);
|
|
243
|
-
if (
|
|
246
|
+
if (contextLoader) {
|
|
244
247
|
lines.push(
|
|
245
|
-
"- `
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
249
|
-
"
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
248
|
+
"- `add_context(source: str) -> dict`: load a dir, file, document, or git URL and **APPEND its",
|
|
249
|
+
" files into `context`** (same shape: path/content/tokens). Documents (PDF, DOCX, XLSX, PPTX,",
|
|
250
|
+
" CSV, …) are converted to Markdown automatically and cached until the source file changes.",
|
|
251
|
+
" Paths are namespaced under `ctx/<source_id>/…` so you can filter by prefix. Returns metadata:",
|
|
252
|
+
" {\"source\", \"source_id\", \"path_prefix\", \"files\", \"chars\", \"context_len\", \"already_loaded\",",
|
|
253
|
+
" \"documents\", \"converted\", \"skipped\"} or an \"Error: ...\" string. `documents` is how many",
|
|
254
|
+
" document-type files landed (incl. cache hits); `converted` is how many were freshly converted",
|
|
255
|
+
" this call. **Never treat the return value as the file list** — always search and chunk the",
|
|
256
|
+
" single variable `context`. Idempotent: re-loading the same source is a no-op.",
|
|
254
257
|
"",
|
|
255
258
|
" ```python",
|
|
256
|
-
|
|
259
|
+
' info = add_context("/path/to/other-project")',
|
|
257
260
|
" # info is metadata; files are already in context under info[\"path_prefix\"]",
|
|
258
|
-
|
|
261
|
+
' lib_files = [f for f in context if f["path"].startswith(info["path_prefix"])]',
|
|
259
262
|
" ```",
|
|
260
263
|
);
|
|
261
264
|
}
|
package/src/prompts/native.ts
CHANGED
|
@@ -17,7 +17,7 @@ function nativeReplGlossary(): string {
|
|
|
17
17
|
"State persists; only `print()` output is returned, so wrap inspections in `print(...)`.",
|
|
18
18
|
"",
|
|
19
19
|
"### REPL Environment",
|
|
20
|
-
"- `context`: list[dict] —
|
|
20
|
+
"- `context`: list[dict] — the files you have loaded (starts empty; cwd seeds on first repl()). Each dict: `path` (str), `content` (str), `tokens` (int).",
|
|
21
21
|
"",
|
|
22
22
|
"Retrieval — free (no sub-LLM call, no tokens). **Start here, before guessing filenames:**",
|
|
23
23
|
"- `search(query, k=10, path_glob=None) -> [{path, line, score, snippet}]` — BM25 over `context`. Returns pointers, not bodies.",
|
|
@@ -30,13 +30,13 @@ function nativeReplGlossary(): string {
|
|
|
30
30
|
"- `llm_query(prompt, model=None) -> str` — one-shot sub-LLM. Use for extraction, summarization, Q&A over a chunk.",
|
|
31
31
|
"- `llm_query_batched(prompts, model=None) -> list[str]` — concurrent sub-LLM calls; output order matches input order.",
|
|
32
32
|
CHUNKED_GLOSSARY_LINE_NATIVE,
|
|
33
|
-
"- `rlm_query(prompt, model=None, paths=None) -> str` — recursive RLM with its own REPL for complex sub-tasks needing iterative reasoning. Prefer llm_query — rlm_query is slower and costlier. The child inherits your `context` (
|
|
33
|
+
"- `rlm_query(prompt, model=None, paths=None) -> str` — recursive RLM with its own REPL for complex sub-tasks needing iterative reasoning. Prefer llm_query — rlm_query is slower and costlier. The child inherits your `context` (loaded files + ctx/ sources) and takes your prompt as its question, so describe the task; never paste file text. `paths=['src/auth/']` narrows its context by prefix.",
|
|
34
34
|
"- `rlm_query_batched(prompts, model=None) -> list[str]` — concurrent recursive RLM calls.",
|
|
35
35
|
"- `spawn(fn, *args) -> Task` / `rlm_await(t)` / `rlm_await_all(ts)` — start `llm_query`, `llm_query_batched`, `llm_query_chunked`, `map_files`, `rlm_query` or `rlm_query_batched` without waiting (NOT `llm_map_reduce`); collect later, order preserved. Tasks outlive the repl() call, so spawn slow work early and await when you need it.",
|
|
36
36
|
"",
|
|
37
37
|
"",
|
|
38
38
|
"- `answers` / `plan` — dicts persisted across every repl() call. Your memo.",
|
|
39
|
-
"- `
|
|
39
|
+
"- `add_context(source) -> dict`: append dir/file/document/git URL into `context` under `ctx/<id>/…`. Documents converted to Markdown. Return is metadata only — always use `context`.",
|
|
40
40
|
"- `SHOW_VARS() -> str` — list all variables currently in the REPL.",
|
|
41
41
|
"- `answer`: dict `{\"content\": \"\", \"ready\": False}`. Setting `answer[\"ready\"] = True` delivers it to the user; do not restate it.",
|
|
42
42
|
"",
|
|
@@ -90,7 +90,7 @@ export function buildNativeSystemPrompt(): string {
|
|
|
90
90
|
"call (rlm_query for iterative sub-tasks). Deterministic Python over `context` is free and",
|
|
91
91
|
"preferred for lookups. Semantic reading is always delegated.",
|
|
92
92
|
"",
|
|
93
|
-
"
|
|
93
|
+
"Loaded file content lives in the REPL `context` variable (cwd seeds on first call). Use ONLY `repl({code})`.",
|
|
94
94
|
"If sub-LLM credits are exhausted → report the error to the user and stop.",
|
|
95
95
|
"",
|
|
96
96
|
"AUTHORING RULE: sub-LLMs (`llm_query` family) READ — they extract, locate, and summarize.",
|
package/src/prompts/system.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface SystemPromptOptions {
|
|
|
28
28
|
readonly orchestrator?: boolean;
|
|
29
29
|
readonly recursion?: boolean;
|
|
30
30
|
readonly maxPromptChars?: number;
|
|
31
|
-
readonly
|
|
31
|
+
readonly contextLoader?: boolean;
|
|
32
32
|
/** depth > 0 — this run is an rlm_query child and its `context` is the parent's world. */
|
|
33
33
|
readonly child?: boolean;
|
|
34
34
|
}
|
|
@@ -70,7 +70,7 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
70
70
|
howToRunCode(),
|
|
71
71
|
"",
|
|
72
72
|
replGlossary(
|
|
73
|
-
kind, recursion, opts.
|
|
73
|
+
kind, recursion, opts.contextLoader ?? false, opts.child ?? false,
|
|
74
74
|
),
|
|
75
75
|
"",
|
|
76
76
|
"REPL stdout over ~800 characters is truncated to a short excerpt — large results stay in your",
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Temp-file transport for sandbox context payloads, with refcounted sharing.
|
|
3
3
|
*
|
|
4
4
|
* Two callers, two ownership models, one writer:
|
|
5
|
-
* - `writeContextTempFile` — non-owning. `
|
|
6
|
-
* that file after reading it (see sandbox.ts serviceInterrupt / worker.py `
|
|
5
|
+
* - `writeContextTempFile` — non-owning. `add_context` uses it because the WORKER unlinks
|
|
6
|
+
* that file after reading it (see sandbox.ts serviceInterrupt / worker.py `_add_context`).
|
|
7
7
|
* - `pinContext` — refcounted. Every child RLM of one node inherits the SAME payload, so an
|
|
8
8
|
* 18-way fan-out would otherwise cost 18 serializations and 18 files. Pins are keyed by
|
|
9
|
-
* payload identity, which is a free version key: `
|
|
10
|
-
* NEW array, so
|
|
9
|
+
* payload identity, which is a free version key: `mergeIntoContext` always returns a
|
|
10
|
+
* NEW array, so adding a source mints a new key and old holders keep their own file.
|
|
11
11
|
*
|
|
12
12
|
* Serialization is chunked with an await between chunks so the event loop is never blocked for
|
|
13
13
|
* more than ~SERIALIZE_CHUNK entries. A Worker Thread was considered and rejected: posting the
|
|
@@ -10,21 +10,27 @@ import type { WorkerInterrupt } from "./protocol.ts";
|
|
|
10
10
|
import { writeContextTempFile } from "./context-file.ts";
|
|
11
11
|
import { errorMessage, formatError } from "../util/errors.ts";
|
|
12
12
|
|
|
13
|
-
/** Result of a host-side
|
|
14
|
-
export interface
|
|
15
|
-
readonly payload: unknown; // always ContextFile[] under
|
|
13
|
+
/** Result of a host-side pack requested by `add_context`. */
|
|
14
|
+
export interface AddContextResult {
|
|
15
|
+
readonly payload: unknown; // always ContextFile[] under ctx/<id>/ (or un-prefixed for cwd)
|
|
16
16
|
readonly files?: number;
|
|
17
17
|
readonly chars: number;
|
|
18
18
|
readonly sourceId: string;
|
|
19
19
|
readonly pathPrefix: string;
|
|
20
|
-
/** Host already has this
|
|
20
|
+
/** Host already has this source — no pack, empty payload. */
|
|
21
21
|
readonly alreadyLoaded?: boolean;
|
|
22
|
+
/** Document-type files in the payload (fresh + cache hits). */
|
|
23
|
+
readonly documents?: number;
|
|
24
|
+
/** Documents freshly converted this call (cache hits excluded). */
|
|
25
|
+
readonly converted?: number;
|
|
26
|
+
/** Paths skipped during packing (model-facing). */
|
|
27
|
+
readonly skipped?: readonly { readonly path: string; readonly reason: string }[];
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
31
|
* Per-interrupt routing context for the sub-LLM handlers.
|
|
26
32
|
*
|
|
27
|
-
* Only the four sub-call kinds can be spawned, so only they carry it;
|
|
33
|
+
* Only the four sub-call kinds can be spawned, so only they carry it; add_context is
|
|
28
34
|
* always synchronous within one exec.
|
|
29
35
|
*/
|
|
30
36
|
export interface SubcallOpts {
|
|
@@ -43,7 +49,7 @@ export interface SubLlmHandlers {
|
|
|
43
49
|
llmQueryBatched(prompts: readonly string[], model: string | null, depth: number, opts: SubcallOpts): Promise<string[]>;
|
|
44
50
|
rlmQuery(prompt: string, model: string | null, depth: number, opts: SubcallOpts): Promise<string>;
|
|
45
51
|
rlmQueryBatched(prompts: readonly string[], model: string | null, depth: number, opts: SubcallOpts): Promise<string[]>;
|
|
46
|
-
|
|
52
|
+
addContext(source: string, depth: number): Promise<AddContextResult>;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
/** Narrow an unknown JSON value to a frozen string array. Non-strings and blanks are dropped. */
|
|
@@ -65,7 +71,7 @@ export const REJECT: SubLlmHandlers = {
|
|
|
65
71
|
llmQueryBatched: async (p) => p.map(() => formatError("sub-LLM bridge not configured")),
|
|
66
72
|
rlmQuery: async () => formatError("sub-LLM bridge not configured"),
|
|
67
73
|
rlmQueryBatched: async (p) => p.map(() => formatError("sub-LLM bridge not configured")),
|
|
68
|
-
|
|
74
|
+
addContext: async () => { throw new Error("add_context not configured"); },
|
|
69
75
|
};
|
|
70
76
|
|
|
71
77
|
/** Body of a reply frame — the union of every handler's payload shape. */
|
|
@@ -79,6 +85,9 @@ export interface ReplyBody {
|
|
|
79
85
|
source_id?: string;
|
|
80
86
|
path_prefix?: string;
|
|
81
87
|
already_loaded?: boolean;
|
|
88
|
+
documents?: number;
|
|
89
|
+
converted?: number;
|
|
90
|
+
skipped?: readonly { readonly path: string; readonly reason: string }[];
|
|
82
91
|
error?: string;
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -114,8 +123,8 @@ export async function serviceInterrupt(
|
|
|
114
123
|
} else if (msg.type === "rlm_query_batched") {
|
|
115
124
|
const responses = await h.rlmQueryBatched(msg.prompts ?? [], msg.model ?? null, d, opts);
|
|
116
125
|
reply(msg.rid, { responses });
|
|
117
|
-
} else if (msg.type === "
|
|
118
|
-
const lib = await h.
|
|
126
|
+
} else if (msg.type === "add_context") {
|
|
127
|
+
const lib = await h.addContext(msg.source ?? "", d);
|
|
119
128
|
if (lib.alreadyLoaded) {
|
|
120
129
|
// No temp file — worker short-circuits on already_loaded.
|
|
121
130
|
reply(msg.rid, {
|
|
@@ -124,10 +133,13 @@ export async function serviceInterrupt(
|
|
|
124
133
|
chars: lib.chars,
|
|
125
134
|
source_id: lib.sourceId,
|
|
126
135
|
path_prefix: lib.pathPrefix,
|
|
136
|
+
documents: lib.documents ?? 0,
|
|
137
|
+
converted: lib.converted ?? 0,
|
|
138
|
+
skipped: lib.skipped,
|
|
127
139
|
});
|
|
128
140
|
} else {
|
|
129
141
|
const { path, json: isJson } = await writeContextTempFile(lib.payload);
|
|
130
|
-
// Worker reads then unlinks (worker.
|
|
142
|
+
// Worker reads then unlinks (worker._add_context). Host must not unlink here —
|
|
131
143
|
// if the worker is SIGKILLed before os.remove, the temp file leaks in tmpdir (acceptable).
|
|
132
144
|
reply(msg.rid, {
|
|
133
145
|
path,
|
|
@@ -136,6 +148,9 @@ export async function serviceInterrupt(
|
|
|
136
148
|
chars: lib.chars,
|
|
137
149
|
source_id: lib.sourceId,
|
|
138
150
|
path_prefix: lib.pathPrefix,
|
|
151
|
+
documents: lib.documents ?? 0,
|
|
152
|
+
converted: lib.converted ?? 0,
|
|
153
|
+
skipped: lib.skipped,
|
|
139
154
|
});
|
|
140
155
|
}
|
|
141
156
|
}
|