@hicaru/pi-rlm 0.1.3 → 0.1.6
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 +1 -0
- package/README.ru.md +1 -0
- package/README.zh-CN.md +1 -0
- package/package.json +1 -1
- package/src/bridge/llm-query.ts +8 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/settings.ts +2 -0
- package/src/context/repomix-context.ts +3 -2
- package/src/core/answer.ts +16 -5
- package/src/core/engine.ts +21 -16
- package/src/core/iteration.ts +7 -1
- package/src/core/pipeline.ts +1 -1
- package/src/core/types.ts +2 -0
- package/src/index.ts +47 -11
- package/src/mode/native-guards.ts +92 -0
- package/src/prompts/system.ts +155 -71
- package/src/registry/edit-registry.ts +22 -0
- package/src/sandbox/protocol.ts +1 -0
- package/src/sandbox/sandbox-manager.ts +9 -2
- package/src/sandbox/sandbox.ts +11 -1
- package/src/sandbox/worker.py +111 -14
- package/src/text/parsing.ts +2 -2
- package/src/tool/apply-edits-tool.ts +126 -0
- package/src/tool/repl-details.ts +3 -1
- package/src/tool/repl-tool.ts +84 -11
- package/src/ui/config-panel.ts +3 -0
package/src/prompts/system.ts
CHANGED
|
@@ -18,8 +18,56 @@ export interface SystemPromptOptions {
|
|
|
18
18
|
readonly recursion?: boolean;
|
|
19
19
|
readonly askUserQuestion?: boolean;
|
|
20
20
|
readonly todo?: boolean;
|
|
21
|
+
readonly pipeline?: boolean;
|
|
22
|
+
readonly maxPromptChars?: number;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
export type ContextKind = "files" | "text";
|
|
26
|
+
|
|
27
|
+
/** "str" (raw string context, e.g. rlm_query children) → text; everything else → files. */
|
|
28
|
+
export function contextKindOf(contextType: string): ContextKind {
|
|
29
|
+
return contextType === "str" ? "text" : "files";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_PROMPT_CAP = 400_000;
|
|
33
|
+
|
|
34
|
+
function promptCapTokensK(maxPromptChars: number): number {
|
|
35
|
+
return Math.round(maxPromptChars / 4_000);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Shared glossary entry for the chunked-query helper (headless + native). */
|
|
39
|
+
const CHUNKED_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
40
|
+
"- `llm_query_chunked(text: str, prompt: str, model=None) -> list[str]`: auto-splits `text` into",
|
|
41
|
+
" chunks that fit the sub-LLM prompt cap, fans them out concurrently (order preserved), and",
|
|
42
|
+
" returns one answer per chunk. Use it for ANY text too large for a single `llm_query` — a file",
|
|
43
|
+
" you open()ed, an oversized sub-result, or several concatenated context files.",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** Why a file the user mentioned may be missing from `context`. */
|
|
47
|
+
const CONTEXT_EXCLUSION_NOTE =
|
|
48
|
+
" NOTE: files larger than 1MB and gitignored files are NOT in `context` — they exist only on disk.";
|
|
49
|
+
|
|
50
|
+
/** The large-on-disk-file protocol (headless + native). */
|
|
51
|
+
const LARGE_FILE_RULE_LINES: readonly string[] = Object.freeze([
|
|
52
|
+
"**Large on-disk files (profiles, logs, dumps, generated JSON):** files >1MB or gitignored are",
|
|
53
|
+
"absent from `context`. Protocol:",
|
|
54
|
+
'1. Load in Python: `raw = open("dhat-heap.json").read()` — loading into a variable is fine.',
|
|
55
|
+
"2. Deterministic processing in Python (`json.load`, `re`, counting, aggregation) is fine and preferred.",
|
|
56
|
+
"3. The moment you need MEANING from raw text (summarize, explain, find anomalies), do NOT read it",
|
|
57
|
+
" yourself — call `llm_query_chunked(raw, question)`, or slice + `llm_query_batched`.",
|
|
58
|
+
"4. Never print more than a small probe (~2K chars) of raw content.",
|
|
59
|
+
'Example: `parts = llm_query_chunked(raw, "Extract top allocation sites with byte totals")`, then',
|
|
60
|
+
"aggregate `parts` in Python or with one final `llm_query`.",
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/** Concise native-mode glossary line for the chunked helper (native prompt has a 6K budget). */
|
|
64
|
+
const CHUNKED_GLOSSARY_LINE_NATIVE =
|
|
65
|
+
"- `llm_query_chunked(text, prompt, model=None) -> list[str]` — auto-splits oversized text into cap-sized chunks, fans out concurrently; one answer per chunk.";
|
|
66
|
+
|
|
67
|
+
/** Concise native-mode large-file rule (folds in the context-exclusion note; native 6K budget). */
|
|
68
|
+
const LARGE_FILE_RULE_NATIVE =
|
|
69
|
+
"- Files >1MB or gitignored are NOT in `context`: open() + parse deterministically in Python is fine; ANY semantic reading of the raw text goes through llm_query_chunked. Never print >2K chars raw.";
|
|
70
|
+
|
|
23
71
|
function howToRunCode(): string {
|
|
24
72
|
return [
|
|
25
73
|
"To run Python, write a fenced ```repl``` block. The REPL **persists** across turns. Only",
|
|
@@ -28,29 +76,41 @@ function howToRunCode(): string {
|
|
|
28
76
|
].join(" ");
|
|
29
77
|
}
|
|
30
78
|
|
|
31
|
-
function replGlossary(recursion: boolean, askUserQuestion: boolean, todo: boolean): string {
|
|
32
|
-
const lines = [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
79
|
+
function replGlossary(kind: ContextKind, recursion: boolean, askUserQuestion: boolean, todo: boolean, pipeline: boolean): string {
|
|
80
|
+
const lines = ["Available in the REPL:"];
|
|
81
|
+
if (kind === "text") {
|
|
82
|
+
lines.push(
|
|
83
|
+
"- `context`: str — the raw text you must analyze. Probe it with slices",
|
|
84
|
+
" (`print(context[:2000])`), split it programmatically, and delegate large chunks",
|
|
85
|
+
" to sub-LLMs — never dump the whole string into your own output.",
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
lines.push(
|
|
89
|
+
"- `context`: list[dict] — a pre-packed JSON array of every file in the repository. Each dict has",
|
|
90
|
+
" keys: `path` (relative file path, str), `content` (file text, str), `tokens` (estimated count, int).",
|
|
91
|
+
" For large repos, chunk `context` into batches and delegate to sub-LLMs — never dump raw file",
|
|
92
|
+
" bodies into your own output.",
|
|
93
|
+
CONTEXT_EXCLUSION_NOTE,
|
|
94
|
+
"",
|
|
95
|
+
" Chunking example:",
|
|
96
|
+
" ```python",
|
|
97
|
+
" chunk_size = 5",
|
|
98
|
+
" for i in range(0, len(context), chunk_size):",
|
|
99
|
+
" batch = context[i:i+chunk_size]",
|
|
100
|
+
" results = llm_query_batched([",
|
|
101
|
+
" f\"Analyze {f['path']} ({f['tokens']} tok):\\n{f['content']}\"",
|
|
102
|
+
" for f in batch",
|
|
103
|
+
" ])",
|
|
104
|
+
" ```",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
lines.push(
|
|
49
108
|
"- `llm_query(prompt: str, model=None) -> str`: a single sub-LLM completion. Use for extraction,",
|
|
50
109
|
" summarization, or Q&A over a chunk of text.",
|
|
51
110
|
"- `llm_query_batched(prompts: list[str], model=None) -> list[str]`: run several sub-LLM calls",
|
|
52
111
|
" concurrently; output order matches input order.",
|
|
53
|
-
|
|
112
|
+
...CHUNKED_GLOSSARY_LINES,
|
|
113
|
+
);
|
|
54
114
|
if (askUserQuestion) {
|
|
55
115
|
lines.push(
|
|
56
116
|
"- `ask_user_question(questions: list[dict]) -> list[dict]`: pause and present the user",
|
|
@@ -86,11 +146,13 @@ function replGlossary(recursion: boolean, askUserQuestion: boolean, todo: boolea
|
|
|
86
146
|
" handle. Avoid excessive recursive sub-calls when a batched one-shot would suffice.",
|
|
87
147
|
);
|
|
88
148
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
149
|
+
if (pipeline) {
|
|
150
|
+
lines.push(
|
|
151
|
+
"- `advance_phase(phase: str, summary=None) -> str`: transition the root RLM pipeline to the next phase.",
|
|
152
|
+
" Valid phases in order: 'research' → 'blueprint' → 'implement' → 'validate'. You must advance forward",
|
|
153
|
+
" one phase at a time. Only callable at the root depth; returns an error in sub-RLM contexts.",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
94
156
|
lines.push(
|
|
95
157
|
"- `SHOW_VARS() -> str`: list every variable currently in the REPL.",
|
|
96
158
|
'- `answer`: a dict initialized to {"content": "", "ready": False}. To submit your final answer,',
|
|
@@ -99,24 +161,26 @@ function replGlossary(recursion: boolean, askUserQuestion: boolean, todo: boolea
|
|
|
99
161
|
return lines.join("\n");
|
|
100
162
|
}
|
|
101
163
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
164
|
+
function orchestratorAddendum(maxPromptChars: number): string {
|
|
165
|
+
return [
|
|
166
|
+
"As an RLM you are an **orchestrator, not a solver**. After you probe `context` and understand the",
|
|
167
|
+
"task, pause and plan: state how the task decomposes into sub-LLM / REPL steps, then execute one step",
|
|
168
|
+
"at a time, printing a small sample of each result to verify before moving on.",
|
|
169
|
+
"",
|
|
170
|
+
"Your own context window is small. Push every long-context operation — reading, summarizing,",
|
|
171
|
+
"classifying, answering sub-questions — into `llm_query` / `llm_query_batched` instead of pulling raw",
|
|
172
|
+
"text into your own message stream. Conversely, if a Python keyword/regex search over `context` would",
|
|
173
|
+
"already pin the answer, just read it directly. Aggregate the small results back in Python.",
|
|
174
|
+
"",
|
|
175
|
+
`Sub-call budget is finite on two axes: (1) per-prompt capacity — each sub-prompt must stay under ${maxPromptChars.toLocaleString()} characters`,
|
|
176
|
+
`(hard cap; ≈${promptCapTokensK(maxPromptChars)}K tokens), packing a chunk of many items per call; (2) batch fan-out —`,
|
|
177
|
+
"keep batches to roughly ~20 prompts. Fat prompts in small batches beat thousands of tiny prompts.",
|
|
178
|
+
"If the workload exceeds both at once, filter aggressively in Python first, then batch the survivors.",
|
|
179
|
+
"",
|
|
180
|
+
"Reserve your own tokens for high-level decisions: what to ask next, how to combine sub-LM outputs,",
|
|
181
|
+
"when to finalize. Delegate everything else. Do not submit a final answer before inspecting `context`.",
|
|
182
|
+
].join("\n");
|
|
183
|
+
}
|
|
120
184
|
|
|
121
185
|
const INTRO = [
|
|
122
186
|
"You are a Recursive Language Model (RLM): a language model with a prompt and a very important",
|
|
@@ -126,12 +190,14 @@ const INTRO = [
|
|
|
126
190
|
/** Build the full RLM system prompt. */
|
|
127
191
|
export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions = {}): string {
|
|
128
192
|
const recursion = opts.recursion ?? false;
|
|
193
|
+
const kind = contextKindOf(meta.contextType);
|
|
194
|
+
const maxPromptChars = opts.maxPromptChars ?? DEFAULT_PROMPT_CAP;
|
|
129
195
|
const parts = [
|
|
130
196
|
INTRO,
|
|
131
197
|
"",
|
|
132
198
|
howToRunCode(),
|
|
133
199
|
"",
|
|
134
|
-
replGlossary(recursion, opts.askUserQuestion ?? false, opts.todo ?? false),
|
|
200
|
+
replGlossary(kind, recursion, opts.askUserQuestion ?? false, opts.todo ?? false, opts.pipeline ?? false),
|
|
135
201
|
"",
|
|
136
202
|
"REPL stdout over ~800 characters is truncated to a short excerpt — large results stay in your",
|
|
137
203
|
"REPL variables as buffers. Re-print only the slice you need (e.g. `print(result[:500])`); never",
|
|
@@ -140,9 +206,12 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
140
206
|
"Start by probing `context` (print a few lines, count items). Then build up an answer to the query.",
|
|
141
207
|
];
|
|
142
208
|
if (opts.orchestrator ?? true) {
|
|
143
|
-
parts.push("",
|
|
209
|
+
parts.push("", orchestratorAddendum(maxPromptChars));
|
|
144
210
|
}
|
|
145
|
-
|
|
211
|
+
if (kind === "files") {
|
|
212
|
+
parts.push("", LARGE_FILE_RULE_LINES.join("\n"));
|
|
213
|
+
}
|
|
214
|
+
parts.push("", buildMetadataLine(meta, maxPromptChars));
|
|
146
215
|
return parts.join("\n");
|
|
147
216
|
}
|
|
148
217
|
|
|
@@ -152,24 +221,20 @@ function nativeReplGlossary(): string {
|
|
|
152
221
|
"## RLM Native Mode — Persistent Python REPL",
|
|
153
222
|
"",
|
|
154
223
|
"Call `repl({code: \"...\"})` to execute Python in a **persistent** sandbox. Variables, imports,",
|
|
155
|
-
"
|
|
156
|
-
"returned, so always wrap inspections in `print(...)`.",
|
|
224
|
+
"State persists; only `print()` output is returned, so wrap inspections in `print(...)`.",
|
|
157
225
|
"",
|
|
158
226
|
"### REPL Environment",
|
|
159
227
|
"- `context`: list[dict] — every file in the repository. Each dict: `path` (str), `content` (str), `tokens` (int).",
|
|
160
228
|
"- `llm_query(prompt, model=None) -> str` — one-shot sub-LLM. Use for extraction, summarization, Q&A over a chunk.",
|
|
161
229
|
"- `llm_query_batched(prompts, model=None) -> list[str]` — concurrent sub-LLM calls; output order matches input order.",
|
|
162
|
-
|
|
230
|
+
CHUNKED_GLOSSARY_LINE_NATIVE,
|
|
231
|
+
"- `rlm_query(prompt, model=None) -> str` — recursive RLM with its own REPL for complex sub-tasks needing iterative reasoning. Prefer llm_query — rlm_query is slower and costlier.",
|
|
163
232
|
"- `rlm_query_batched(prompts, model=None) -> list[str]` — concurrent recursive RLM calls.",
|
|
164
233
|
"",
|
|
165
|
-
"**Choosing between `llm_query` and `rlm_query`:** default to `llm_query`/batched; use `rlm_query` only for iterative sub-tasks.",
|
|
166
234
|
"- `todo(action, **kwargs) -> str` — manage a task list. Actions: create, update, list, get, delete, clear. Statuses: pending → in_progress → completed.",
|
|
167
235
|
"- `SHOW_VARS() -> str` — list all variables currently in the REPL.",
|
|
168
|
-
"- `stage_edit(path, old_text, new_text) -> str`:
|
|
169
|
-
"
|
|
170
|
-
" stage_edit once per file. The repl() result will include a STAGED_EDITS JSON block.",
|
|
171
|
-
" The main agent must then call `edit` for each entry verbatim — zero analysis needed.",
|
|
172
|
-
"- `answer`: dict `{\"content\": \"\", \"ready\": False}`. To submit: `answer[\"content\"] = \"...\"; answer[\"ready\"] = True`.",
|
|
236
|
+
"- `stage_edit(path, old_text, new_text) -> str`: stages an edit and returns an edit ID; apply IDs with `apply_edits`.",
|
|
237
|
+
"- `answer`: dict `{\"content\": \"\", \"ready\": False}`. Setting `answer[\"ready\"] = True` delivers it to the user; do not restate it.",
|
|
173
238
|
"",
|
|
174
239
|
"### Orchestrator Pattern",
|
|
175
240
|
"You are an **orchestrator, not a solver**. After probing `context`, decompose the task into sub-LLM / REPL steps,",
|
|
@@ -190,30 +255,28 @@ function nativeReplGlossary(): string {
|
|
|
190
255
|
" ])",
|
|
191
256
|
" # aggregate results into a buffer",
|
|
192
257
|
"```",
|
|
193
|
-
|
|
194
|
-
"- If your `context` is small enough (<20 files), you CAN read files directly via `read` / `grep` / `zebra-mcp`.",
|
|
195
|
-
"- For medium/large repos, delegate to sub-LLMs via the REPL.",
|
|
258
|
+
`- Keep sub-prompts under ${DEFAULT_PROMPT_CAP.toLocaleString()} characters (≈${promptCapTokensK(DEFAULT_PROMPT_CAP)}K tokens); batch ~20 prompts per call. Fat prompts in small batches > thousands of tiny prompts.`,
|
|
196
259
|
"",
|
|
197
260
|
"### Choosing Between Tools",
|
|
198
261
|
"| Tool | When |",
|
|
199
262
|
"|------|------|",
|
|
200
263
|
"| `repl({code})` | Need to chunk/delegate `context` to sub-LLMs; need Python scripting; need REPL state across calls |",
|
|
201
|
-
"| `read` / `grep` | Inspect a few specific files directly; small codebase |",
|
|
202
264
|
"| `zebra-mcp` | Semantic search over the codebase |",
|
|
203
265
|
"| `edit` | Modify an existing file with exact text replacement (native Pi flow, visible to all plugins) |",
|
|
204
266
|
"| `write` | Create a new file (native Pi flow, visible to all plugins) |",
|
|
205
267
|
"| `llm_query` (inside repl) | Extract, summarize, or classify a chunk of text |",
|
|
206
268
|
"| `rlm_query` (inside repl) | Complex sub-task needing iterative reasoning with its own REPL |",
|
|
207
269
|
"| `todo` (inside repl) | Track multi-step progress visibly to the user |",
|
|
208
|
-
"| `stage_edit(path, old, new)` (inside repl) |
|
|
270
|
+
"| `stage_edit(path, old, new)` (inside repl) | Stage exact edit params; apply returned IDs with `apply_edits` |",
|
|
209
271
|
"",
|
|
210
272
|
"### Workflow",
|
|
211
273
|
"1. **Plan**: Create todos for the multi-step analysis. Probe `context` — print length, inspect a few entries.",
|
|
212
274
|
"2. **Chunk & Delegate**: Slice `context` into batches, delegate each batch to sub-LLMs via `llm_query_batched`.",
|
|
213
275
|
"3. **Aggregate**: Collect results in Python, pass aggregated results to a final `llm_query` or produce the answer directly.",
|
|
214
|
-
"4. **Finalize**: For file changes, stage them inside repl() via stage_edit(path, old, new)
|
|
276
|
+
"4. **Finalize**: For file changes, stage them inside repl() via `stage_edit(path, old, new)`, then apply the returned IDs with `apply_edits({ ids })`. For analysis tasks, write a normal message.",
|
|
215
277
|
"",
|
|
216
278
|
"### Task-Specific Patterns",
|
|
279
|
+
LARGE_FILE_RULE_NATIVE,
|
|
217
280
|
"- Architecture/code review: chunk relevant files and delegate summaries or review to `llm_query_batched`.",
|
|
218
281
|
"- Bug investigation: use Python string/regex search over `context`; delegate matching files for analysis.",
|
|
219
282
|
"- If sub-LLM credits are exhausted, report partial results and stop — do not bypass REPL restrictions.",
|
|
@@ -230,29 +293,50 @@ export function buildNativeSystemPrompt(): string {
|
|
|
230
293
|
"║ NATIVE RLM MODE — YOU ARE AN ORCHESTRATOR, NOT A READER ║",
|
|
231
294
|
"╚══════════════════════════════════════════════════════════════════╝",
|
|
232
295
|
"",
|
|
233
|
-
"
|
|
296
|
+
"ENFORCED BY THE RUNTIME (not advisory):",
|
|
297
|
+
"- `read`/`grep` are blocked; bash readers (cat/sed/head/tail/awk/rg) are blocked; bash output is hard-capped at 4K chars.",
|
|
298
|
+
"- repl() stdout returned to you is hard-capped at 4K chars — printing file bodies is USELESS; the text will not reach you.",
|
|
299
|
+
"",
|
|
300
|
+
"DELEGATION RULE: if a step needs MEANING from more than ~4K chars of text, that reading MUST",
|
|
301
|
+
"be an llm_query / llm_query_batched / llm_query_chunked call (rlm_query for iterative",
|
|
302
|
+
"sub-tasks). Deterministic Python (search, count, slice, json, re) over `context` is free and",
|
|
303
|
+
"preferred for lookups. Semantic reading is always delegated.",
|
|
304
|
+
"",
|
|
234
305
|
"All file content is pre-loaded in the REPL `context` variable. Use ONLY `repl({code})`.",
|
|
235
306
|
"If sub-LLM credits are exhausted → report the error to the user and stop.",
|
|
236
307
|
"",
|
|
237
|
-
"For file changes,
|
|
238
|
-
"
|
|
239
|
-
"",
|
|
240
|
-
"When repl() returns a STAGED_EDITS block, apply each entry by calling `edit` verbatim:",
|
|
241
|
-
" edit({ path: entry.path, edits: [{ oldText: entry.oldText, newText: entry.newText }] })",
|
|
242
|
-
"Do not analyze or modify the parameters — relay them exactly as provided by the sub-agent.",
|
|
308
|
+
"For file changes, prefer `stage_edit()` inside repl(); it returns edit IDs and keeps edit bodies out of your output.",
|
|
309
|
+
"When repl() returns STAGED_EDITS, apply them with `apply_edits({ ids: [\"e1\", ...] })`.",
|
|
310
|
+
"Never re-type file paths, oldText, newText, file bodies, or `answer[\"content\"]` in your own output.",
|
|
243
311
|
"",
|
|
244
312
|
nativeReplGlossary(),
|
|
245
313
|
].join("\n");
|
|
246
314
|
}
|
|
247
315
|
|
|
316
|
+
/** Soft cap on the static native prompt. Leaves headroom for per-turn context injection
|
|
317
|
+
* without bloating the root model's system prompt. Exceeded → phase-guards.ts fails. */
|
|
318
|
+
export const NATIVE_PROMPT_BUDGET = 6_000;
|
|
319
|
+
|
|
248
320
|
/** Exported for tests — prompt length without context metadata (which is injected separately). */
|
|
249
321
|
export const NATIVE_PROMPT_STATIC = buildNativeSystemPrompt();
|
|
250
322
|
|
|
323
|
+
/** Per-turn last-position reminder for native mode — appended to every context build. */
|
|
324
|
+
export const NATIVE_TURN_REMINDER = [
|
|
325
|
+
"[RLM orchestrator contract — enforced by the runtime, not optional:",
|
|
326
|
+
"repl() stdout to you is hard-capped at 4K chars; read/grep and bash readers are blocked.",
|
|
327
|
+
"Any SEMANTIC reading of file/text content MUST go through llm_query / llm_query_batched /",
|
|
328
|
+
"llm_query_chunked (rlm_query for iterative sub-tasks). Deterministic Python (search, count,",
|
|
329
|
+
"slice, json) is free. Keep your own output to decisions and aggregation.]",
|
|
330
|
+
].join("\n");
|
|
331
|
+
|
|
251
332
|
/** The one-line context metadata, also reused by the per-turn prompt in headless mode. */
|
|
252
|
-
export function buildMetadataLine(meta: PromptMeta): string {
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
333
|
+
export function buildMetadataLine(meta: PromptMeta, maxPromptChars = DEFAULT_PROMPT_CAP): string {
|
|
334
|
+
const kind = contextKindOf(meta.contextType);
|
|
335
|
+
const contextDesc = kind === "text"
|
|
336
|
+
? `Your context is a plain string of ${meta.contextChars.toLocaleString()} characters. Use Python slicing to chunk it for sub-LLM delegation.`
|
|
337
|
+
: `Your context is a JSON array of ${meta.contextChars.toLocaleString()} total characters — list[dict] where each dict has keys "path" (str), "content" (str), and "tokens" (int). Use Python list slicing to chunk it into batches for sub-LLM delegation.`;
|
|
338
|
+
const tail = `Each sub-LLM call accepts up to ${maxPromptChars.toLocaleString()} characters (≈${promptCapTokensK(maxPromptChars)}K tokens).`;
|
|
339
|
+
const dist = kind === "files" && meta.contextStats
|
|
256
340
|
? ` Your context has ${meta.contextStats.files} files; per-file tokens run min ${meta.contextStats.min.toLocaleString()} / median ${meta.contextStats.median.toLocaleString()} / max ${meta.contextStats.max.toLocaleString()} — use this to gauge how many files fit per batch.`
|
|
257
341
|
: "";
|
|
258
342
|
const body = `${contextDesc} ${tail}${dist}`;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ProposedEdit } from "../sandbox/protocol.ts";
|
|
2
|
+
|
|
3
|
+
export class EditRegistry {
|
|
4
|
+
private readonly edits = new Map<string, ProposedEdit>();
|
|
5
|
+
|
|
6
|
+
registerAll(edits: readonly ProposedEdit[] | undefined): void {
|
|
7
|
+
if (edits === undefined) return;
|
|
8
|
+
for (const edit of edits) this.edits.set(edit.id, edit);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get(id: string): ProposedEdit | undefined {
|
|
12
|
+
return this.edits.get(id);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
delete(id: string): boolean {
|
|
16
|
+
return this.edits.delete(id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
clear(): void {
|
|
20
|
+
this.edits.clear();
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/sandbox/protocol.ts
CHANGED
|
@@ -13,7 +13,9 @@ export interface SandboxManagerConfig {
|
|
|
13
13
|
readonly requestTimeoutMs: number;
|
|
14
14
|
readonly python: string;
|
|
15
15
|
readonly sandboxInitTimeoutMs: number;
|
|
16
|
+
readonly maxPromptChars: number;
|
|
16
17
|
readonly signal?: AbortSignal;
|
|
18
|
+
readonly onSandboxDiscarded?: () => void;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export class SandboxManager {
|
|
@@ -58,6 +60,7 @@ export class SandboxManager {
|
|
|
58
60
|
python: this.config.python,
|
|
59
61
|
signal: this.config.signal,
|
|
60
62
|
initTimeoutMs: this.config.sandboxInitTimeoutMs,
|
|
63
|
+
maxPromptChars: this.config.maxPromptChars,
|
|
61
64
|
handlers,
|
|
62
65
|
}).then(async (s) => {
|
|
63
66
|
// Load context on first creation if available.
|
|
@@ -116,6 +119,7 @@ export class SandboxManager {
|
|
|
116
119
|
try { await this.sandbox.dispose(); } catch { /* already dead */ }
|
|
117
120
|
this.sandbox = null;
|
|
118
121
|
this.contextLoaded = false;
|
|
122
|
+
this.config.onSandboxDiscarded?.();
|
|
119
123
|
}
|
|
120
124
|
throw err;
|
|
121
125
|
} finally {
|
|
@@ -139,7 +143,10 @@ export class SandboxManager {
|
|
|
139
143
|
if (this.disposed) return;
|
|
140
144
|
this.disposed = true;
|
|
141
145
|
await this.sandbox?.dispose();
|
|
142
|
-
this.sandbox
|
|
143
|
-
|
|
146
|
+
if (this.sandbox !== null) {
|
|
147
|
+
this.sandbox = null;
|
|
148
|
+
this.contextLoaded = false;
|
|
149
|
+
this.config.onSandboxDiscarded?.();
|
|
150
|
+
}
|
|
144
151
|
}
|
|
145
152
|
}
|
package/src/sandbox/sandbox.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface SandboxOptions {
|
|
|
52
52
|
readonly signal?: AbortSignal;
|
|
53
53
|
/** Worker startup wait before init failure (ms). */
|
|
54
54
|
readonly initTimeoutMs?: number;
|
|
55
|
+
/** Sub-LLM prompt cap (chars) — sizes llm_query_chunked chunks inside the worker. */
|
|
56
|
+
readonly maxPromptChars?: number;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
const WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), "worker.py");
|
|
@@ -110,9 +112,17 @@ export class PythonSandbox {
|
|
|
110
112
|
this.requestTimeoutMs = opts.requestTimeoutMs ?? 20 * 60_000;
|
|
111
113
|
this.initTimeoutMs = opts.initTimeoutMs ?? 30_000;
|
|
112
114
|
const python = opts.python ?? "python3";
|
|
115
|
+
const workerArgs = [
|
|
116
|
+
"-u", WORKER_PATH,
|
|
117
|
+
"--depth", String(opts.depth ?? 1),
|
|
118
|
+
"--timeout", String(opts.execTimeoutS ?? 600),
|
|
119
|
+
];
|
|
120
|
+
if (opts.maxPromptChars !== undefined) {
|
|
121
|
+
workerArgs.push("--max-prompt-chars", String(opts.maxPromptChars));
|
|
122
|
+
}
|
|
113
123
|
this.proc = spawn(
|
|
114
124
|
python,
|
|
115
|
-
|
|
125
|
+
workerArgs,
|
|
116
126
|
{ stdio: ["pipe", "pipe", "pipe"], env: sanitizedEnv() },
|
|
117
127
|
) as ChildProcessWithoutNullStreams;
|
|
118
128
|
|