@hicaru/pi-rlm 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -4
- package/package.json +2 -1
- package/src/bridge/library.ts +155 -0
- package/src/bridge/llm-query.ts +1 -0
- package/src/bridge/rlm-query.ts +56 -12
- package/src/config/defaults.ts +2 -0
- package/src/config/settings.ts +4 -0
- package/src/context/library-context.ts +266 -0
- package/src/context/repomix-context.ts +2 -48
- package/src/core/answer.ts +1 -10
- package/src/core/artifacts.ts +88 -0
- package/src/core/critique.ts +92 -0
- package/src/core/engine.ts +446 -53
- package/src/core/gates.ts +301 -0
- package/src/core/iteration.ts +7 -2
- package/src/core/pipeline.ts +196 -28
- package/src/core/types.ts +5 -3
- package/src/index.ts +3 -6
- package/src/mode/native-guards.ts +2 -2
- package/src/prompts/phases.ts +104 -0
- package/src/prompts/system.ts +59 -16
- package/src/prompts/user.ts +12 -4
- package/src/sandbox/protocol.ts +29 -11
- package/src/sandbox/sandbox.ts +77 -2
- package/src/sandbox/worker.py +215 -46
- package/src/state/index.ts +2 -1
- package/src/state/paths.ts +4 -2
- package/src/state/reads.ts +31 -2
- package/src/state/resume.ts +31 -6
- package/src/state/rows.ts +8 -2
- package/src/state/writes.ts +5 -3
- package/src/text/tokens.ts +7 -1
- package/src/tool/repl-details.ts +2 -3
- package/src/tool/repl-tool.ts +52 -57
- package/src/tool/rlm-aggregator.ts +7 -7
- package/src/tool/rlm-details.ts +6 -3
- package/src/tool/rlm-events.ts +14 -11
- package/src/tool/rlm-tool.ts +2 -8
- package/src/tool/subcall-store.ts +2 -0
- package/src/ui/config-panel.ts +8 -1
- package/src/registry/edit-registry.ts +0 -22
- package/src/text/edits.ts +0 -16
- package/src/tool/apply-edits-tool.ts +0 -288
package/src/index.ts
CHANGED
|
@@ -7,8 +7,6 @@ import { registerRlmCommand } from "./commands/rlm.ts";
|
|
|
7
7
|
import { registerRlmConfigCommand } from "./commands/rlm-config.ts";
|
|
8
8
|
import { createRlmTool } from "./tool/rlm-tool.ts";
|
|
9
9
|
import { createReplTool } from "./tool/repl-tool.ts";
|
|
10
|
-
import { createApplyEditsTool } from "./tool/apply-edits-tool.ts";
|
|
11
|
-
import { EditRegistry } from "./registry/edit-registry.ts";
|
|
12
10
|
import { loadSettings, mergeConfig, resolveModelId } from "./config/settings.ts";
|
|
13
11
|
import { RlmController, cheapestModel } from "./mode/rlm-mode.ts";
|
|
14
12
|
import { postRlmGuide } from "./ui/intro.ts";
|
|
@@ -26,14 +24,14 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
26
24
|
// Init synchronously with defaults — ensures commands/tools/handlers register before session_start
|
|
27
25
|
const config = mergeConfig({});
|
|
28
26
|
const controller = new RlmController(config);
|
|
29
|
-
|
|
27
|
+
let onSandboxDiscardExtra: (() => void) | undefined;
|
|
30
28
|
const sandboxManager = new SandboxManager({
|
|
31
29
|
execTimeoutS: config.execTimeoutS,
|
|
32
30
|
requestTimeoutMs: config.requestTimeoutMs,
|
|
33
31
|
python: config.python,
|
|
34
32
|
sandboxInitTimeoutMs: config.sandboxInitTimeoutMs,
|
|
35
33
|
maxPromptChars: config.maxPromptChars,
|
|
36
|
-
onSandboxDiscarded: () => {
|
|
34
|
+
onSandboxDiscarded: () => { onSandboxDiscardExtra?.(); },
|
|
37
35
|
});
|
|
38
36
|
let packedContextText: string | undefined;
|
|
39
37
|
let contextPackPromise: Promise<string | undefined> | undefined;
|
|
@@ -81,7 +79,6 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
81
79
|
|
|
82
80
|
// ── Tool registration ──
|
|
83
81
|
pi.registerTool(createRlmTool(controller));
|
|
84
|
-
pi.registerTool(createApplyEditsTool(editRegistry));
|
|
85
82
|
let guidePosted = false;
|
|
86
83
|
|
|
87
84
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -105,8 +102,8 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
105
102
|
getModel: () => controller.resolveModels(ctx)?.model,
|
|
106
103
|
getWorkerModel: () => controller.resolveModels(ctx)?.worker,
|
|
107
104
|
registry: ctx.modelRegistry,
|
|
108
|
-
editRegistry,
|
|
109
105
|
config: controller.config,
|
|
106
|
+
registerDiscardHook: (reset) => { onSandboxDiscardExtra = reset; },
|
|
110
107
|
ensureContext: async () => {
|
|
111
108
|
const contextText = await ensureRepositoryContext(ctx.cwd ?? process.cwd());
|
|
112
109
|
if (contextText === undefined) throw new Error("repository context could not be loaded into RLM sandbox");
|
|
@@ -86,7 +86,7 @@ export function replDelegationNudge(stdoutChars: number, delegated: boolean): st
|
|
|
86
86
|
if (delegated || stdoutChars <= NUDGE_STDOUT_CHARS) return undefined;
|
|
87
87
|
return (
|
|
88
88
|
`\n[RLM: this repl() printed ${stdoutChars.toLocaleString()} chars with 0 sub-LLM calls — ` +
|
|
89
|
-
"
|
|
90
|
-
"
|
|
89
|
+
"if you were READING, delegate via llm_query / llm_query_batched / llm_query_chunked. " +
|
|
90
|
+
"Authoring an edit body yourself is correct and needs no delegation.]"
|
|
91
91
|
);
|
|
92
92
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-phase guidance for the gated RLM pipeline.
|
|
3
|
+
* Injected at each phase boundary; the only channel between phases is durable
|
|
4
|
+
* artifacts under .rlm/artifacts/.
|
|
5
|
+
*/
|
|
6
|
+
import type { Phase } from "../core/pipeline.ts";
|
|
7
|
+
|
|
8
|
+
const CLARIFY_GUIDANCE = Object.freeze([
|
|
9
|
+
"## Clarify phase (intake interview)",
|
|
10
|
+
"Interview the user until the task is understood. The engine COUNTS ask_user_question",
|
|
11
|
+
"rounds it services — you cannot advance without having actually asked at least once.",
|
|
12
|
+
"",
|
|
13
|
+
"1. Intent first: ask ONE open-ended intent question (what problem, who hits it, what",
|
|
14
|
+
" success looks like). Options are answer *shapes* (e.g. end user / maintainer / operator);",
|
|
15
|
+
" do NOT mark a Recommended option. The automatic Other / free-text path carries the real framing.",
|
|
16
|
+
" Up to 2 more intent rounds if the answer still cannot scope a narrow probe; then proceed",
|
|
17
|
+
" with what you have.",
|
|
18
|
+
"2. Probe before asking more: ground follow-ups in the REPL (deterministic search over",
|
|
19
|
+
" context, a few llm_query reads) so questions cite real code as file:line.",
|
|
20
|
+
"3. Confirm inferences, don't record them: batch inferred decisions into one call —",
|
|
21
|
+
" \"From the code I inferred <behavior> (file:line). Keep or change?\" The user's answer",
|
|
22
|
+
" is the Decision, not your inference.",
|
|
23
|
+
"4. Scope / shape / detail rounds, one at a time: recommended option first; architecture",
|
|
24
|
+
" (shape) options must state the tradeoff (\"optimizes X, costs Y\") — never one option",
|
|
25
|
+
" masquerading as a choice; 2–4 independent detail questions may batch in one call.",
|
|
26
|
+
"5. Classify answers: Decision → record; Correction → re-probe that seam, re-ask dependents;",
|
|
27
|
+
" Defer → ## Open Questions.",
|
|
28
|
+
"6. Terminate on depth, not politeness: stop when every raised branch has a Decision or a",
|
|
29
|
+
" Deferral and ## Problem & Intent holds the user's own words verbatim. Do not pad the",
|
|
30
|
+
" interview; do not ask a final \"looks good?\" rubber-stamp.",
|
|
31
|
+
"7. Then save_artifact(\"clarification\", content) with frontmatter:",
|
|
32
|
+
" status: ready",
|
|
33
|
+
" decisions_count: N",
|
|
34
|
+
" open_questions_count: M",
|
|
35
|
+
" and body sections:",
|
|
36
|
+
" ## Problem & Intent (user's words VERBATIM)",
|
|
37
|
+
" ## Decisions (one '- ' bullet per decision)",
|
|
38
|
+
" ## Open Questions (one '- ' bullet per deferral; may be empty with count 0)",
|
|
39
|
+
" ## Non-Goals",
|
|
40
|
+
' Then advance_phase("research", summary).',
|
|
41
|
+
].join("\n"));
|
|
42
|
+
|
|
43
|
+
const RESEARCH_GUIDANCE = Object.freeze([
|
|
44
|
+
"## Research phase",
|
|
45
|
+
"Read the clarifications artifact (if present) and honor recorded Decisions; do not silently",
|
|
46
|
+
"resolve Open Questions.",
|
|
47
|
+
"Probe the repository, delegate long reads via `llm_query_batched` / `llm_query_chunked`.",
|
|
48
|
+
"Every factual claim about the code MUST be cited as `path/file.ext:LINE` (or `LINE-LINE`).",
|
|
49
|
+
"The engine VERIFIES citations against the working tree — unbacked citations reject advance.",
|
|
50
|
+
"When research is complete, write ONE research document with a `## Findings` section and call:",
|
|
51
|
+
' `save_artifact("research", content)` # frontmatter must include `status: ready`',
|
|
52
|
+
' `advance_phase("blueprint", summary)`',
|
|
53
|
+
"Do not implement code in this phase.",
|
|
54
|
+
].join("\n"));
|
|
55
|
+
|
|
56
|
+
const BLUEPRINT_GUIDANCE = Object.freeze([
|
|
57
|
+
"## Blueprint phase",
|
|
58
|
+
"Read the clarifications artifact (if present) and plan only within recorded Decisions;",
|
|
59
|
+
"Open Questions that would block the design must be re-asked via ask_user_question, not assumed.",
|
|
60
|
+
'Produce ONE plan document and save it with `save_artifact("plan", content)`.',
|
|
61
|
+
"This pipeline is read-only: it produces a plan a human or a native-mode agent executes;",
|
|
62
|
+
"do not write source files here (sandbox write modes are blocked).",
|
|
63
|
+
"Write changes as exact, copy-pasteable before/after code — never as prose instructions.",
|
|
64
|
+
"The ENGINE derive-checks the document — these are hard gates, not suggestions:",
|
|
65
|
+
"- frontmatter: `status: ready`, `phase_count: N`, `phases: [{n: 1, title: ...}, ...]`",
|
|
66
|
+
"- `phases:` / `phase_count` MUST match the `## Phase N:` body headings exactly (fenced examples ignored)",
|
|
67
|
+
"- every `file:line` citation MUST resolve against the working tree at this revision",
|
|
68
|
+
"Each `## Phase N: <title>` section contains:",
|
|
69
|
+
"- `### Changes Required` — per file: path + the exact intended change",
|
|
70
|
+
"- `### Success Criteria` — `#### Automated Verification:` (runnable commands) and",
|
|
71
|
+
" `#### Manual Verification:` checklists",
|
|
72
|
+
"Phases must be independently implementable in order, each leaving the tree working.",
|
|
73
|
+
'When ready: `advance_phase("validate", summary)`.',
|
|
74
|
+
].join("\n"));
|
|
75
|
+
|
|
76
|
+
const VALIDATE_GUIDANCE = Object.freeze([
|
|
77
|
+
"## Validate phase (adversarial plan review)",
|
|
78
|
+
"Nothing has been implemented — you are reviewing the PLAN, not a diff.",
|
|
79
|
+
"Read the goal artifact (verbatim brief), the clarifications artifact (recorded Decisions),",
|
|
80
|
+
"and the plan. For each plan phase check against the working tree:",
|
|
81
|
+
"- every cited `file:line` still resolves and says what the plan claims",
|
|
82
|
+
"- every file the phase edits exists (or is created by an earlier phase)",
|
|
83
|
+
"- no phase contradicts a recorded Decision or silently resolves an Open Question",
|
|
84
|
+
"- the Success Criteria are actually runnable and actually prove the change",
|
|
85
|
+
"- phases are independently implementable in the stated order",
|
|
86
|
+
"Write ONE review via `save_artifact(\"validation\", content)` with frontmatter:",
|
|
87
|
+
"- `status: ready`",
|
|
88
|
+
"- `blockers_count: <int >= 0>` # MEASURED — routing key; not prose",
|
|
89
|
+
"- `verdict: pass | fail` # pass requires blockers_count === 0",
|
|
90
|
+
"Each blocker needs a resolvable `file:line` citation.",
|
|
91
|
+
"Then finalize: `answer[\"content\"] = <the reviewed plan + review summary>; answer[\"ready\"] = True`.",
|
|
92
|
+
"If `blockers_count > 0`, the engine re-enters blueprint (bounded by maxBackwardJumps).",
|
|
93
|
+
].join("\n"));
|
|
94
|
+
|
|
95
|
+
const GUIDANCE: Readonly<Record<Phase, string>> = Object.freeze({
|
|
96
|
+
clarify: CLARIFY_GUIDANCE,
|
|
97
|
+
research: RESEARCH_GUIDANCE,
|
|
98
|
+
blueprint: BLUEPRINT_GUIDANCE,
|
|
99
|
+
validate: VALIDATE_GUIDANCE,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export function phaseGuidance(phase: Phase): string {
|
|
103
|
+
return GUIDANCE[phase];
|
|
104
|
+
}
|
package/src/prompts/system.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface SystemPromptOptions {
|
|
|
20
20
|
readonly todo?: boolean;
|
|
21
21
|
readonly pipeline?: boolean;
|
|
22
22
|
readonly maxPromptChars?: number;
|
|
23
|
+
readonly libraryLoader?: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export type ContextKind = "files" | "text";
|
|
@@ -76,7 +77,14 @@ function howToRunCode(): string {
|
|
|
76
77
|
].join(" ");
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
function replGlossary(
|
|
80
|
+
function replGlossary(
|
|
81
|
+
kind: ContextKind,
|
|
82
|
+
recursion: boolean,
|
|
83
|
+
askUserQuestion: boolean,
|
|
84
|
+
todo: boolean,
|
|
85
|
+
pipeline: boolean,
|
|
86
|
+
libraryLoader: boolean,
|
|
87
|
+
): string {
|
|
80
88
|
const lines = ["Available in the REPL:"];
|
|
81
89
|
if (kind === "text") {
|
|
82
90
|
lines.push(
|
|
@@ -116,8 +124,9 @@ function replGlossary(kind: ContextKind, recursion: boolean, askUserQuestion: bo
|
|
|
116
124
|
"- `ask_user_question(questions: list[dict]) -> list[dict]`: pause and present the user",
|
|
117
125
|
" with 1-4 structured questions. Each question: {question, header, options: [{label, description}],",
|
|
118
126
|
" multiSelect?}. Returns list of {question, selected: [label], custom?}.",
|
|
119
|
-
"
|
|
120
|
-
"
|
|
127
|
+
" Default: use concrete options grounded in code/data (2–4 choices, Recommended first when ranking).",
|
|
128
|
+
" Exception — clarify-phase intent rounds: lead with an open-ended intent question whose options",
|
|
129
|
+
" are answer *shapes* (not a Recommended pick); free-text / Other carries the real framing.",
|
|
121
130
|
" Only valid at root depth; returns an error inside rlm_query sub-calls.",
|
|
122
131
|
);
|
|
123
132
|
}
|
|
@@ -130,6 +139,25 @@ function replGlossary(kind: ContextKind, recursion: boolean, askUserQuestion: bo
|
|
|
130
139
|
" Use to plan multi-step work before starting, then mark tasks as you complete them.",
|
|
131
140
|
);
|
|
132
141
|
}
|
|
142
|
+
if (libraryLoader) {
|
|
143
|
+
lines.push(
|
|
144
|
+
"- `load_library(source: str) -> dict`: load an EXTERNAL library, source tree, or document and",
|
|
145
|
+
" **APPEND its files into the existing `context` list** (same shape: path/content/tokens).",
|
|
146
|
+
" `source` may be a local directory (repomix-packed), a single file path, or an https/git@ URL",
|
|
147
|
+
" (shallow-cloned, then packed). Paths are namespaced under `lib/<source_id>/…` so you can filter",
|
|
148
|
+
" by prefix. Returns metadata only:",
|
|
149
|
+
" {\"source\", \"source_id\", \"path_prefix\", \"files\", \"chars\", \"context_len\", \"already_loaded\"}",
|
|
150
|
+
" or an \"Error: ...\" string. **Never treat the return value as the file list** — always search",
|
|
151
|
+
" and chunk the single variable `context`. Do not invent `context_1` / aliases; do not call",
|
|
152
|
+
" globals()/locals(). Idempotent: re-loading the same source is a no-op.",
|
|
153
|
+
"",
|
|
154
|
+
" ```python",
|
|
155
|
+
" info = load_library(\"/path/to/other-project\")",
|
|
156
|
+
" # info is metadata; files are already in context under info[\"path_prefix\"]",
|
|
157
|
+
" lib_files = [f for f in context if f[\"path\"].startswith(info[\"path_prefix\"])]",
|
|
158
|
+
" ```",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
133
161
|
if (recursion) {
|
|
134
162
|
lines.push(
|
|
135
163
|
"- `rlm_query(prompt, model=None)` / `rlm_query_batched(prompts, model=None)`: recursive RLM",
|
|
@@ -148,9 +176,16 @@ function replGlossary(kind: ContextKind, recursion: boolean, askUserQuestion: bo
|
|
|
148
176
|
}
|
|
149
177
|
if (pipeline) {
|
|
150
178
|
lines.push(
|
|
151
|
-
"- `
|
|
152
|
-
"
|
|
153
|
-
"
|
|
179
|
+
"- `save_artifact(kind: str, content: str) -> str`: persist a stage artifact under `.rlm/artifacts/`.",
|
|
180
|
+
" Kinds: `'clarification'` | `'research'` | `'plan'` | `'validation'`. Must match the current phase.",
|
|
181
|
+
" Frontmatter must eventually include `status: ready` before `advance_phase` will accept the transition.",
|
|
182
|
+
"- `advance_phase(phase: str, summary=None) -> str`: transition to the next pipeline phase.",
|
|
183
|
+
" Order: 'clarify' → 'research' → 'blueprint' → 'validate' (one step at a time;",
|
|
184
|
+
" clarify is skipped when ask_user_question is disabled). The pipeline is READ-ONLY.",
|
|
185
|
+
" **advance_phase is validated by the engine** — it measures the latest saved artifact",
|
|
186
|
+
" (status, structure, citations, blockers_count; clarify also requires ≥1 ask_user_question round).",
|
|
187
|
+
" A rejected transition returns the gate error for you to fix; the phase does NOT advance.",
|
|
188
|
+
" Only callable at root depth.",
|
|
154
189
|
);
|
|
155
190
|
}
|
|
156
191
|
lines.push(
|
|
@@ -197,7 +232,10 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
197
232
|
"",
|
|
198
233
|
howToRunCode(),
|
|
199
234
|
"",
|
|
200
|
-
replGlossary(
|
|
235
|
+
replGlossary(
|
|
236
|
+
kind, recursion, opts.askUserQuestion ?? false, opts.todo ?? false,
|
|
237
|
+
opts.pipeline ?? false, opts.libraryLoader ?? false,
|
|
238
|
+
),
|
|
201
239
|
"",
|
|
202
240
|
"REPL stdout over ~800 characters is truncated to a short excerpt — large results stay in your",
|
|
203
241
|
"REPL variables as buffers. Re-print only the slice you need (e.g. `print(result[:500])`); never",
|
|
@@ -232,8 +270,8 @@ function nativeReplGlossary(): string {
|
|
|
232
270
|
"- `rlm_query_batched(prompts, model=None) -> list[str]` — concurrent recursive RLM calls.",
|
|
233
271
|
"",
|
|
234
272
|
"- `todo(action, **kwargs) -> str` — manage a task list. Actions: create, update, list, get, delete, clear. Statuses: pending → in_progress → completed.",
|
|
273
|
+
"- `load_library(source) -> dict`: append external dir/file/git tree into `context` under `lib/<id>/…`. Return is metadata only — always use `context`.",
|
|
235
274
|
"- `SHOW_VARS() -> str` — list all variables currently in the REPL.",
|
|
236
|
-
"- `stage_edit(path, old_text, new_text) -> str`: stages an edit and returns an edit ID; apply IDs with `apply_edits`.",
|
|
237
275
|
"- `answer`: dict `{\"content\": \"\", \"ready\": False}`. Setting `answer[\"ready\"] = True` delivers it to the user; do not restate it.",
|
|
238
276
|
"",
|
|
239
277
|
"### Orchestrator Pattern",
|
|
@@ -262,18 +300,19 @@ function nativeReplGlossary(): string {
|
|
|
262
300
|
"|------|------|",
|
|
263
301
|
"| `repl({code})` | Need to chunk/delegate `context` to sub-LLMs; need Python scripting; need REPL state across calls |",
|
|
264
302
|
"| `zebra-mcp` | Semantic search over the codebase |",
|
|
265
|
-
"| `edit` |
|
|
266
|
-
"| `write` | Create a new file
|
|
303
|
+
"| `edit` | Change an existing file. Compose oldText/newText yourself; exact match required |",
|
|
304
|
+
"| `write` | Create a new file |",
|
|
267
305
|
"| `llm_query` (inside repl) | Extract, summarize, or classify a chunk of text |",
|
|
268
306
|
"| `rlm_query` (inside repl) | Complex sub-task needing iterative reasoning with its own REPL |",
|
|
269
307
|
"| `todo` (inside repl) | Track multi-step progress visibly to the user |",
|
|
270
|
-
"| `stage_edit(path, old, new)` (inside repl) | Stage exact edit params; apply returned IDs with `apply_edits` |",
|
|
271
308
|
"",
|
|
272
309
|
"### Workflow",
|
|
273
310
|
"1. **Plan**: Create todos for the multi-step analysis. Probe `context` — print length, inspect a few entries.",
|
|
274
311
|
"2. **Chunk & Delegate**: Slice `context` into batches, delegate each batch to sub-LLMs via `llm_query_batched`.",
|
|
275
312
|
"3. **Aggregate**: Collect results in Python, pass aggregated results to a final `llm_query` or produce the answer directly.",
|
|
276
|
-
"4. **Finalize**: For file changes,
|
|
313
|
+
"4. **Finalize**: For file changes, call the native `edit` / `write` tools directly — you",
|
|
314
|
+
" author the change, and Pi validates the anchor and renders the diff. For analysis tasks,",
|
|
315
|
+
" write a normal message.",
|
|
277
316
|
"",
|
|
278
317
|
"### Task-Specific Patterns",
|
|
279
318
|
LARGE_FILE_RULE_NATIVE,
|
|
@@ -305,9 +344,11 @@ export function buildNativeSystemPrompt(): string {
|
|
|
305
344
|
"All file content is pre-loaded in the REPL `context` variable. Use ONLY `repl({code})`.",
|
|
306
345
|
"If sub-LLM credits are exhausted → report the error to the user and stop.",
|
|
307
346
|
"",
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
"
|
|
347
|
+
"AUTHORING RULE: sub-LLMs (`llm_query` family) READ — they extract, locate, and summarize.",
|
|
348
|
+
"They never author code you will ship. Once you know WHAT to change, compose the exact",
|
|
349
|
+
"oldText / newText yourself and apply it with `edit`. Delegated code is written by a small",
|
|
350
|
+
"model with no view of the codebase; it is a research aid, never a patch.",
|
|
351
|
+
"Never re-type file bodies or `answer[\"content\"]` in your own output.",
|
|
311
352
|
"",
|
|
312
353
|
nativeReplGlossary(),
|
|
313
354
|
].join("\n");
|
|
@@ -326,7 +367,9 @@ export const NATIVE_TURN_REMINDER = [
|
|
|
326
367
|
"repl() stdout to you is hard-capped at 4K chars; read/grep and bash readers are blocked.",
|
|
327
368
|
"Any SEMANTIC reading of file/text content MUST go through llm_query / llm_query_batched /",
|
|
328
369
|
"llm_query_chunked (rlm_query for iterative sub-tasks). Deterministic Python (search, count,",
|
|
329
|
-
"slice, json) is free.
|
|
370
|
+
"slice, json) is free. AUTHORING IS NOT READING: you write every edit body yourself and apply",
|
|
371
|
+
"it with the native `edit` / `write` tools — never delegate code you will ship to a sub-LLM.",
|
|
372
|
+
"Keep your own output to decisions, authored edits, and aggregation.]",
|
|
330
373
|
].join("\n");
|
|
331
374
|
|
|
332
375
|
/** The one-line context metadata, also reused by the per-turn prompt in headless mode. */
|
package/src/prompts/user.ts
CHANGED
|
@@ -3,16 +3,24 @@
|
|
|
3
3
|
* Native mode does not use these — pi's own loop supplies the turns.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export function buildTurnPrompt(
|
|
6
|
+
export function buildTurnPrompt(
|
|
7
|
+
iteration: number,
|
|
8
|
+
maxIterations: number,
|
|
9
|
+
gateMessage?: string,
|
|
10
|
+
phaseGuidanceText?: string,
|
|
11
|
+
): string {
|
|
12
|
+
const parts: string[] = [];
|
|
13
|
+
if (phaseGuidanceText) parts.push(phaseGuidanceText);
|
|
14
|
+
if (gateMessage) parts.push(gateMessage);
|
|
15
|
+
const prefix = parts.length > 0 ? `${parts.join("\n\n")}\n\n` : "";
|
|
7
16
|
const body = `Turn ${iteration + 1}/${maxIterations}:`;
|
|
8
|
-
const gate = gateMessage ? `\n${gateMessage}\n\n` : "";
|
|
9
17
|
if (iteration === 0) {
|
|
10
18
|
return (
|
|
11
19
|
"You have not interacted with the REPL or seen your context yet. Look at the context first; " +
|
|
12
|
-
`do not provide a final answer yet.\n\n${
|
|
20
|
+
`do not provide a final answer yet.\n\n${prefix}${body}`
|
|
13
21
|
);
|
|
14
22
|
}
|
|
15
|
-
return `${
|
|
23
|
+
return `${prefix}${body}`;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
/** Asked once when the engine runs out of turns without a submitted answer. */
|
package/src/sandbox/protocol.ts
CHANGED
|
@@ -21,18 +21,21 @@ export interface LlmReply {
|
|
|
21
21
|
readonly response?: string;
|
|
22
22
|
readonly responses?: readonly string[];
|
|
23
23
|
readonly answers?: readonly AskAnswer[];
|
|
24
|
+
/** load_library reply: temp file with the packed payload (+ resume index / namespace). */
|
|
25
|
+
readonly path?: string;
|
|
26
|
+
readonly json?: boolean;
|
|
27
|
+
readonly index?: number;
|
|
28
|
+
readonly files?: number;
|
|
29
|
+
readonly chars?: number;
|
|
30
|
+
readonly source_id?: string;
|
|
31
|
+
readonly path_prefix?: string;
|
|
32
|
+
/** Host-side idempotency: library already loaded — no path payload. */
|
|
33
|
+
readonly already_loaded?: boolean;
|
|
24
34
|
readonly error?: string;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
export type ParentMessage = WorkerRequest | LlmReply;
|
|
28
38
|
|
|
29
|
-
export interface ProposedEdit {
|
|
30
|
-
readonly id: string;
|
|
31
|
-
readonly path: string;
|
|
32
|
-
readonly oldText: string;
|
|
33
|
-
readonly newText: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
39
|
/** A normal response to a request (keyed by the request `id`). */
|
|
37
40
|
export interface WorkerResponse {
|
|
38
41
|
readonly id: string;
|
|
@@ -43,7 +46,6 @@ export interface WorkerResponse {
|
|
|
43
46
|
readonly stderr?: string;
|
|
44
47
|
readonly final_answer?: string | null;
|
|
45
48
|
readonly answer_content?: string;
|
|
46
|
-
readonly edits?: readonly ProposedEdit[];
|
|
47
49
|
readonly raised?: boolean;
|
|
48
50
|
readonly execution_time?: number;
|
|
49
51
|
// user-created variable names after this exec (filters builtins/context) — Metadata(stdout) for history orientation
|
|
@@ -62,8 +64,10 @@ export type InterruptKind =
|
|
|
62
64
|
| "rlm_query"
|
|
63
65
|
| "rlm_query_batched"
|
|
64
66
|
| "advance_phase"
|
|
67
|
+
| "save_artifact"
|
|
65
68
|
| "ask_user_question"
|
|
66
|
-
| "todo"
|
|
69
|
+
| "todo"
|
|
70
|
+
| "load_library";
|
|
67
71
|
|
|
68
72
|
export interface AskOption {
|
|
69
73
|
readonly label: string;
|
|
@@ -111,6 +115,12 @@ interface AdvancePhaseInterrupt extends InterruptBase {
|
|
|
111
115
|
readonly summary?: string;
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
interface SaveArtifactInterrupt extends InterruptBase {
|
|
119
|
+
readonly type: "save_artifact";
|
|
120
|
+
readonly artifactKind?: string;
|
|
121
|
+
readonly content?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
export interface AskUserQuestionInterrupt extends InterruptBase {
|
|
115
125
|
readonly type: "ask_user_question";
|
|
116
126
|
readonly questions: readonly AskQuestion[];
|
|
@@ -132,13 +142,20 @@ export interface TodoInterrupt extends InterruptBase {
|
|
|
132
142
|
readonly includeDeleted?: boolean;
|
|
133
143
|
}
|
|
134
144
|
|
|
145
|
+
export interface LoadLibraryInterrupt extends InterruptBase {
|
|
146
|
+
readonly type: "load_library";
|
|
147
|
+
readonly source?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
135
150
|
/** A mid-exec sub-LLM/tool request from the worker. */
|
|
136
151
|
export type WorkerInterrupt =
|
|
137
152
|
| PromptInterrupt
|
|
138
153
|
| BatchedPromptInterrupt
|
|
139
154
|
| AdvancePhaseInterrupt
|
|
155
|
+
| SaveArtifactInterrupt
|
|
140
156
|
| AskUserQuestionInterrupt
|
|
141
|
-
| TodoInterrupt
|
|
157
|
+
| TodoInterrupt
|
|
158
|
+
| LoadLibraryInterrupt;
|
|
142
159
|
|
|
143
160
|
export type WorkerMessage = WorkerResponse | WorkerInterrupt;
|
|
144
161
|
|
|
@@ -148,8 +165,10 @@ export const INTERRUPT_KINDS = Object.freeze(new Set<InterruptKind>([
|
|
|
148
165
|
"rlm_query",
|
|
149
166
|
"rlm_query_batched",
|
|
150
167
|
"advance_phase",
|
|
168
|
+
"save_artifact",
|
|
151
169
|
"ask_user_question",
|
|
152
170
|
"todo",
|
|
171
|
+
"load_library",
|
|
153
172
|
]));
|
|
154
173
|
|
|
155
174
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -178,7 +197,6 @@ export interface ReplResult {
|
|
|
178
197
|
readonly stderr: string;
|
|
179
198
|
readonly finalAnswer: string | null;
|
|
180
199
|
readonly answerContent: string;
|
|
181
|
-
readonly edits: readonly ProposedEdit[];
|
|
182
200
|
readonly raised: boolean;
|
|
183
201
|
readonly executionTimeMs: number;
|
|
184
202
|
/** User-created variable names after this exec (builtins/context filtered out). */
|
package/src/sandbox/sandbox.ts
CHANGED
|
@@ -26,6 +26,18 @@ import {
|
|
|
26
26
|
} from "./protocol.ts";
|
|
27
27
|
import { formatError } from "../util/errors.ts";
|
|
28
28
|
|
|
29
|
+
/** Result of a host-side library pack requested by `load_library`. */
|
|
30
|
+
export interface LibraryLoadResult {
|
|
31
|
+
readonly payload: unknown; // always ContextFile[] under lib/<id>/
|
|
32
|
+
readonly index: number; // resume-sidecar index (not a REPL var name); -1 if alreadyLoaded
|
|
33
|
+
readonly files?: number;
|
|
34
|
+
readonly chars: number;
|
|
35
|
+
readonly sourceId: string;
|
|
36
|
+
readonly pathPrefix: string;
|
|
37
|
+
/** Host already has this library — no pack, no sidecar, empty payload. */
|
|
38
|
+
readonly alreadyLoaded?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
/** Handlers the bridge installs to service sub-LLM interrupts. Return the reply payload. */
|
|
30
42
|
export interface SubLlmHandlers {
|
|
31
43
|
llmQuery(prompt: string, model: string | null, depth: number): Promise<string>;
|
|
@@ -33,8 +45,10 @@ export interface SubLlmHandlers {
|
|
|
33
45
|
rlmQuery(prompt: string, model: string | null, depth: number): Promise<string>;
|
|
34
46
|
rlmQueryBatched(prompts: readonly string[], model: string | null, depth: number): Promise<string[]>;
|
|
35
47
|
advancePhase(phase: string, summary: string | undefined, depth: number): Promise<string>;
|
|
48
|
+
saveArtifact(kind: string, content: string, depth: number): Promise<string>;
|
|
36
49
|
askUserQuestion(questions: readonly AskQuestion[], depth: number): Promise<AskAnswer[]>;
|
|
37
50
|
todo(action: string, params: Record<string, unknown>, depth: number): Promise<string>;
|
|
51
|
+
loadLibrary(source: string, depth: number): Promise<LibraryLoadResult>;
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
export interface SandboxOptions {
|
|
@@ -54,6 +68,11 @@ export interface SandboxOptions {
|
|
|
54
68
|
readonly initTimeoutMs?: number;
|
|
55
69
|
/** Sub-LLM prompt cap (chars) — sizes llm_query_chunked chunks inside the worker. */
|
|
56
70
|
readonly maxPromptChars?: number;
|
|
71
|
+
/**
|
|
72
|
+
* When true, the worker rejects open() write modes (pipeline read-only runs).
|
|
73
|
+
* Native repl() data work leaves this false so scratch-file writes still work.
|
|
74
|
+
*/
|
|
75
|
+
readonly readOnly?: boolean;
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
const WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), "worker.py");
|
|
@@ -76,12 +95,14 @@ const REJECT: SubLlmHandlers = {
|
|
|
76
95
|
rlmQuery: async () => formatError("sub-LLM bridge not configured"),
|
|
77
96
|
rlmQueryBatched: async (p) => p.map(() => formatError("sub-LLM bridge not configured")),
|
|
78
97
|
advancePhase: async () => formatError("phase advancement not available"),
|
|
98
|
+
saveArtifact: async () => formatError("save_artifact not available"),
|
|
79
99
|
askUserQuestion: async (questions) => questions.map((q) => ({
|
|
80
100
|
question: q.question,
|
|
81
101
|
selected: [],
|
|
82
102
|
custom: formatError("ask_user_question not configured"),
|
|
83
103
|
})),
|
|
84
104
|
todo: async () => formatError("todo not configured"),
|
|
105
|
+
loadLibrary: async () => { throw new Error("load_library not configured"); },
|
|
85
106
|
};
|
|
86
107
|
|
|
87
108
|
/** Distributive omit so each union member keeps its own fields (plain Omit collapses to shared keys). */
|
|
@@ -120,6 +141,9 @@ export class PythonSandbox {
|
|
|
120
141
|
if (opts.maxPromptChars !== undefined) {
|
|
121
142
|
workerArgs.push("--max-prompt-chars", String(opts.maxPromptChars));
|
|
122
143
|
}
|
|
144
|
+
if (opts.readOnly) {
|
|
145
|
+
workerArgs.push("--read-only");
|
|
146
|
+
}
|
|
123
147
|
this.proc = spawn(
|
|
124
148
|
python,
|
|
125
149
|
workerArgs,
|
|
@@ -200,7 +224,6 @@ export class PythonSandbox {
|
|
|
200
224
|
stderr: res.stderr ?? "",
|
|
201
225
|
finalAnswer: res.final_answer ?? null,
|
|
202
226
|
answerContent: res.answer_content ?? "",
|
|
203
|
-
edits: res.edits ?? [],
|
|
204
227
|
raised: res.raised ?? false,
|
|
205
228
|
executionTimeMs: Math.round((res.execution_time ?? 0) * 1000),
|
|
206
229
|
varNames: res.var_names ?? [],
|
|
@@ -285,6 +308,15 @@ export class PythonSandbox {
|
|
|
285
308
|
}
|
|
286
309
|
}
|
|
287
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Refresh the parent-side request watchdog for every pending request.
|
|
313
|
+
* Used during long mid-exec work that does not
|
|
314
|
+
* produce additional worker interrupts on this sandbox.
|
|
315
|
+
*/
|
|
316
|
+
refreshWatchdog(): void {
|
|
317
|
+
this.touchPending();
|
|
318
|
+
}
|
|
319
|
+
|
|
288
320
|
private send(msg: ParentMessage): void {
|
|
289
321
|
this.proc.stdin.write(`${JSON.stringify(msg)}\n`);
|
|
290
322
|
}
|
|
@@ -346,6 +378,9 @@ export class PythonSandbox {
|
|
|
346
378
|
} else if (msg.type === "advance_phase") {
|
|
347
379
|
const response = await h.advancePhase(msg.phase ?? "", msg.summary, d);
|
|
348
380
|
this.reply(msg.rid, { response });
|
|
381
|
+
} else if (msg.type === "save_artifact") {
|
|
382
|
+
const response = await h.saveArtifact(msg.artifactKind ?? "", msg.content ?? "", d);
|
|
383
|
+
this.reply(msg.rid, { response });
|
|
349
384
|
} else if (msg.type === "ask_user_question") {
|
|
350
385
|
const answers = await h.askUserQuestion(msg.questions ?? [], d);
|
|
351
386
|
this.reply(msg.rid, { answers });
|
|
@@ -355,13 +390,53 @@ export class PythonSandbox {
|
|
|
355
390
|
);
|
|
356
391
|
const response = await h.todo(msg.action ?? "list", params, d);
|
|
357
392
|
this.reply(msg.rid, { response });
|
|
393
|
+
} else if (msg.type === "load_library") {
|
|
394
|
+
const lib = await h.loadLibrary(msg.source ?? "", d);
|
|
395
|
+
if (lib.alreadyLoaded) {
|
|
396
|
+
// No temp file — worker short-circuits on already_loaded.
|
|
397
|
+
this.reply(msg.rid, {
|
|
398
|
+
already_loaded: true,
|
|
399
|
+
index: lib.index,
|
|
400
|
+
files: 0,
|
|
401
|
+
chars: lib.chars,
|
|
402
|
+
source_id: lib.sourceId,
|
|
403
|
+
path_prefix: lib.pathPrefix,
|
|
404
|
+
});
|
|
405
|
+
} else {
|
|
406
|
+
const isJson = typeof lib.payload !== "string";
|
|
407
|
+
const path = await this.writeContextFile(lib.payload, isJson);
|
|
408
|
+
// Worker reads then unlinks (worker._load_library). Host must not unlink here —
|
|
409
|
+
// if the worker is SIGKILLed before os.remove, the temp file leaks in tmpdir (acceptable).
|
|
410
|
+
this.reply(msg.rid, {
|
|
411
|
+
path,
|
|
412
|
+
json: isJson,
|
|
413
|
+
index: lib.index,
|
|
414
|
+
files: lib.files,
|
|
415
|
+
chars: lib.chars,
|
|
416
|
+
source_id: lib.sourceId,
|
|
417
|
+
path_prefix: lib.pathPrefix,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
358
420
|
}
|
|
359
421
|
} catch (err) {
|
|
360
422
|
this.reply(msg.rid, { error: err instanceof Error ? err.message : String(err) });
|
|
361
423
|
}
|
|
362
424
|
}
|
|
363
425
|
|
|
364
|
-
private reply(rid: string, body: {
|
|
426
|
+
private reply(rid: string, body: {
|
|
427
|
+
response?: string;
|
|
428
|
+
responses?: string[];
|
|
429
|
+
answers?: AskAnswer[];
|
|
430
|
+
path?: string;
|
|
431
|
+
json?: boolean;
|
|
432
|
+
index?: number;
|
|
433
|
+
files?: number;
|
|
434
|
+
chars?: number;
|
|
435
|
+
source_id?: string;
|
|
436
|
+
path_prefix?: string;
|
|
437
|
+
already_loaded?: boolean;
|
|
438
|
+
error?: string;
|
|
439
|
+
}): void {
|
|
365
440
|
if (!this.disposed) this.send({ type: "llm_reply", rid, ...body });
|
|
366
441
|
}
|
|
367
442
|
|