@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.
Files changed (47) hide show
  1. package/README.md +20 -16
  2. package/README.ru.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/package.json +22 -19
  5. package/src/bridge/add-context.ts +322 -0
  6. package/src/bridge/subcall-handlers.ts +1 -1
  7. package/src/config/defaults.ts +2 -1
  8. package/src/config/settings.ts +5 -2
  9. package/src/context/anydoc.ts +67 -0
  10. package/src/context/listing.ts +70 -0
  11. package/src/context/md-cache.ts +112 -0
  12. package/src/context/merge.ts +97 -0
  13. package/src/context/namespace.ts +180 -0
  14. package/src/context/resolve.ts +122 -0
  15. package/src/context/source-dir.ts +166 -0
  16. package/src/context/source-doc.ts +71 -0
  17. package/src/context/source-git.ts +51 -0
  18. package/src/context/source-text.ts +45 -0
  19. package/src/context/types.ts +88 -0
  20. package/src/context/walk.ts +250 -0
  21. package/src/core/engine.ts +15 -19
  22. package/src/core/types.ts +7 -2
  23. package/src/index.ts +69 -42
  24. package/src/mode/rlm-mode.ts +5 -4
  25. package/src/prompts/glossary.ts +31 -28
  26. package/src/prompts/native.ts +4 -4
  27. package/src/prompts/system.ts +2 -2
  28. package/src/sandbox/context-file.ts +4 -4
  29. package/src/sandbox/interrupts.ts +25 -10
  30. package/src/sandbox/protocol.ts +13 -7
  31. package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
  32. package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
  33. package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
  34. package/src/sandbox/py/guards.py +1 -1
  35. package/src/sandbox/py/retrieval.py +1 -1
  36. package/src/sandbox/py/tasks.py +17 -4
  37. package/src/sandbox/py/worker.py +68 -48
  38. package/src/sandbox/sandbox-manager.ts +18 -16
  39. package/src/sandbox/sandbox.ts +1 -1
  40. package/src/text/tokens.ts +3 -3
  41. package/src/tool/repl-details.ts +1 -1
  42. package/src/tool/repl-tool.ts +31 -19
  43. package/src/tool/rlm-tool.ts +1 -1
  44. package/src/ui/config-panel.ts +8 -4
  45. package/src/bridge/library.ts +0 -190
  46. package/src/context/library-context.ts +0 -339
  47. package/src/context/repomix-context.ts +0 -204
@@ -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 { buildLibraryHandler } from "../bridge/library.ts";
13
- import { mergeLibraryIntoContext } from "../context/library-context.ts";
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 library loaded on turn 3 must reach a child spawned on turn 4. Safe
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: the repo pack plus every library loaded so far. Children inherit
157
- * it, so it must grow when load_library appends (see the library handler's onLoaded below).
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 = null;
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 (mergeLibraryIntoContext returns a new array). */
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
- libraryLoader: deps.config.libraryLoader,
190
+ contextLoader: deps.config.contextLoader,
192
191
  child: input.depth > 0,
193
192
  });
194
193
 
195
- const libraryHandlers = deps.config.libraryLoader
196
- ? buildLibraryHandler({
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 library too.
205
- liveContext = mergeLibraryIntoContext(liveContext, payload);
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, ...libraryHandlers },
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: serialize ContextBundle to sandbox-ready JSON array, pass raw strings through.
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 load_library() REPL scaffold (external dirs/files/git repos as extra context slots). */
46
- readonly libraryLoader: boolean;
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 { packRepository, formatForLLM, serializeForSandbox } from "./context/repomix-context.ts";
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
- let packedContextText: string | undefined;
61
- let contextPackPromise: Promise<string | undefined> | undefined;
62
- const ensureRepositoryContext = async (cwd: string): Promise<string | undefined> => {
63
- if (packedContextText !== undefined && sandboxManager.contextPayload !== null) return packedContextText;
64
- contextPackPromise ??= packRepository(cwd)
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] repository context pack failed: ${result.error}`);
68
- return undefined;
85
+ console.warn(`[rlm] context seed failed: ${result.error}`);
86
+ return;
69
87
  }
70
- sandboxManager.contextPayload = serializeForSandbox(result.value);
71
- packedContextText = formatForLLM(result.value);
72
- return packedContextText;
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(() => { contextPackPromise = undefined; });
75
- return contextPackPromise;
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
- const contextText = await ensureRepositoryContext(ctx.cwd ?? process.cwd());
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: repo listing for the main agent ──
188
- let contextInjected = false;
189
- pi.on("context", async (event, ctx) => {
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
- // Inject repository context as a compact listing (once per session)
200
- if (!contextInjected) {
201
- const cwd = ctx.cwd ?? process.cwd();
202
- const contextText = await ensureRepositoryContext(cwd);
203
- if (contextText !== undefined) {
204
- contextInjected = true;
205
- const instruction = [
206
- "ANALYZE THIS REPOSITORY using repl({code}) read/grep are DISABLED.",
207
- "Repository contents are pre-loaded in the Python REPL `context` variable.",
208
- "Locate with search()/grep_context()/outline() (free), then delegate bulk reading to",
209
- "map_files()/llm_query_batched(). If credits exhausted → report and stop.",
210
- "",
211
- ].join("\n");
212
- filtered.unshift({
213
- role: "user" as const,
214
- content: instruction + contextText,
215
- timestamp: 0,
216
- } as PiMessage);
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 — all files are pre-loaded in the REPL `context` variable. Use `edit`/`write` for file changes. If sub-LLM credits are exhausted, report to the user.",
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
- contextInjected = false;
268
- packedContextText = undefined;
269
- contextPackPromise = undefined;
270
- sandboxManager.contextPayload = null;
292
+ cwdSeeded = false;
293
+ seedPromise = undefined;
294
+ contextBundleRef = undefined;
295
+ listingPayloadRef = undefined;
296
+ listingInjected = false;
297
+ sandboxManager.contextPayload = [];
271
298
  });
272
299
  }
@@ -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 { packRepository, serializeForSandbox } from "../context/repomix-context.ts";
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-pack empty/undefined context via repomix; pass explicit context through.
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 packRepository(cwd, abortController.signal);
95
+ const result = await resolveSource(cwd, { cwd, pathPrefix: "", signal: abortController.signal });
95
96
  contextValue = result.ok
96
- ? serializeForSandbox(result.value)
97
+ ? result.value.payload
97
98
  : formatError(`failed to pack repository — ${result.error}`);
98
99
  }
99
100
  const engine = createEngine({
@@ -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` — the repository plus every library you",
86
- " loaded (`lib/<id>/…`) — and runs `search` / `grep_context` / `outline` / `map_files` over the",
87
- " same paths. So send instructions, never file bodies: pasting content you already share costs",
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/', 'lib/x-9f3a/'])` — path PREFIXES,",
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: libraries the child loads, and its whole REPL, die with it — only its",
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 — the repository plus every library",
103
- " it loaded (paths under `lib/<id>/…`). Answer only the question above; your REPL and anything",
104
- " you load die with you, and only your final answer string returns to the parent.",
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: files larger than 1MB and gitignored files are NOT in `context` they exist only on disk.";
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
- libraryLoader: boolean,
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] — a pre-packed JSON array of every file in the repository. Each dict has",
216
- " keys: `path` (relative file path, str), `content` (file text, str), `tokens` (estimated count, int).",
217
- " For large repos, chunk `context` into batches and delegate to sub-LLMs — never dump raw file",
218
- " bodies into your own output.",
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 (libraryLoader) {
246
+ if (contextLoader) {
244
247
  lines.push(
245
- "- `load_library(source: str) -> dict`: load an EXTERNAL library, source tree, or document and",
246
- " **APPEND its files into the existing `context` list** (same shape: path/content/tokens).",
247
- " `source` may be a local directory (repomix-packed), a single file path, or an https/git@ URL",
248
- " (shallow-cloned, then packed). Paths are namespaced under `lib/<source_id>/…` so you can filter",
249
- " by prefix. Returns metadata only:",
250
- " {\"source\", \"source_id\", \"path_prefix\", \"files\", \"chars\", \"context_len\", \"already_loaded\"}",
251
- " or an \"Error: ...\" string. **Never treat the return value as the file list** — always search",
252
- " and chunk the single variable `context`. Do not invent `context_1` / aliases; do not call",
253
- " globals()/locals(). Idempotent: re-loading the same source is a no-op.",
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
- " info = load_library(\"/path/to/other-project\")",
259
+ ' info = add_context("/path/to/other-project")',
257
260
  " # info is metadata; files are already in context under info[\"path_prefix\"]",
258
- " lib_files = [f for f in context if f[\"path\"].startswith(info[\"path_prefix\"])]",
261
+ ' lib_files = [f for f in context if f["path"].startswith(info["path_prefix"])]',
259
262
  " ```",
260
263
  );
261
264
  }
@@ -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] — every file in the repository. Each dict: `path` (str), `content` (str), `tokens` (int).",
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` (repo + loaded libraries) and takes your prompt as its question, so describe the task; never paste file text. `paths=['src/auth/']` narrows its context by prefix.",
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
- "- `load_library(source) -> dict`: append external dir/file/git tree into `context` under `lib/<id>/…`. Return is metadata only — always use `context`.",
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
- "All file content is pre-loaded in the REPL `context` variable. Use ONLY `repl({code})`.",
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.",
@@ -28,7 +28,7 @@ export interface SystemPromptOptions {
28
28
  readonly orchestrator?: boolean;
29
29
  readonly recursion?: boolean;
30
30
  readonly maxPromptChars?: number;
31
- readonly libraryLoader?: boolean;
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.libraryLoader ?? false, opts.child ?? false,
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. `load_library` uses it because the WORKER unlinks
6
- * that file after reading it (see sandbox.ts serviceInterrupt / worker.py `_load_library`).
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: `mergeLibraryIntoContext` always returns a
10
- * NEW array, so loading a library mints a new key and old holders keep their own file.
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 library pack requested by `load_library`. */
14
- export interface LibraryLoadResult {
15
- readonly payload: unknown; // always ContextFile[] under lib/<id>/
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 library — no pack, empty payload. */
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; load_library is
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
- loadLibrary(source: string, depth: number): Promise<LibraryLoadResult>;
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
- loadLibrary: async () => { throw new Error("load_library not configured"); },
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 === "load_library") {
118
- const lib = await h.loadLibrary(msg.source ?? "", d);
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._load_library). Host must not unlink here —
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
  }