@hicaru/pi-rlm 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -47
- package/README.ru.md +18 -23
- package/README.zh-CN.md +17 -28
- package/package.json +22 -19
- package/src/bridge/add-context.ts +322 -0
- package/src/bridge/subcall-handlers.ts +63 -17
- package/src/commands/rlm-config.ts +47 -18
- package/src/commands/rlm.ts +3 -152
- package/src/config/defaults.ts +8 -18
- package/src/config/settings.ts +13 -34
- package/src/context/anydoc.ts +67 -0
- package/src/context/listing.ts +70 -0
- package/src/context/md-cache.ts +112 -0
- package/src/context/merge.ts +97 -0
- package/src/context/namespace.ts +180 -0
- package/src/context/resolve.ts +122 -0
- package/src/context/source-dir.ts +166 -0
- package/src/context/source-doc.ts +71 -0
- package/src/context/source-git.ts +51 -0
- package/src/context/source-text.ts +45 -0
- package/src/context/types.ts +88 -0
- package/src/context/walk.ts +250 -0
- package/src/core/engine.ts +61 -345
- package/src/core/history.ts +1 -1
- package/src/core/limits.ts +5 -12
- package/src/core/resource-limits.ts +0 -2
- package/src/core/types.ts +10 -38
- package/src/index.ts +92 -54
- package/src/mode/llm-model.ts +54 -0
- package/src/mode/rlm-mode.ts +28 -58
- package/src/prompts/glossary.ts +290 -0
- package/src/prompts/native.ts +127 -0
- package/src/prompts/system.ts +15 -408
- package/src/sandbox/context-file.ts +154 -0
- package/src/sandbox/interrupts.ts +160 -0
- package/src/sandbox/protocol.ts +20 -75
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +150 -0
- package/src/sandbox/py/retrieval.py +265 -0
- package/src/sandbox/py/tasks.py +129 -0
- package/src/sandbox/py/worker.py +856 -0
- package/src/sandbox/sandbox-manager.ts +24 -9
- package/src/sandbox/sandbox.ts +99 -193
- package/src/text/tokens.ts +31 -5
- package/src/tool/repl-details.ts +2 -2
- package/src/tool/repl-render.ts +58 -0
- package/src/tool/repl-result.ts +70 -0
- package/src/tool/repl-tool.ts +60 -170
- package/src/tool/rlm-aggregator.ts +2 -10
- package/src/tool/rlm-details.ts +0 -2
- package/src/tool/rlm-events.ts +0 -14
- package/src/tool/rlm-tool.ts +2 -13
- package/src/ui/config-panel.ts +12 -20
- package/src/ui/intro.ts +1 -2
- package/src/ui/model-picker.ts +34 -10
- package/src/ui/status.ts +3 -7
- package/src/util/concurrency.ts +9 -5
- package/src/bridge/fallback-todo.ts +0 -148
- package/src/bridge/interactive.ts +0 -65
- package/src/bridge/library.ts +0 -155
- package/src/bridge/pi-interactive.ts +0 -41
- package/src/context/library-context.ts +0 -266
- package/src/context/repomix-context.ts +0 -204
- package/src/core/artifacts.ts +0 -89
- package/src/core/critique.ts +0 -92
- package/src/core/gates.ts +0 -301
- package/src/core/pipeline-handlers.ts +0 -319
- package/src/core/pipeline.ts +0 -268
- package/src/prompts/phases.ts +0 -104
- package/src/sandbox/worker.py +0 -1456
- package/src/state/index.ts +0 -24
- package/src/state/internal.ts +0 -46
- package/src/state/paths.ts +0 -44
- package/src/state/reads.ts +0 -133
- package/src/state/resume.ts +0 -173
- package/src/state/rows.ts +0 -123
- package/src/state/writes.ts +0 -58
package/src/core/types.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/** Shared configuration + runtime types for the RLM engine. */
|
|
2
2
|
|
|
3
3
|
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { AskAnswer, AskQuestion } from "../sandbox/protocol.ts";
|
|
5
|
-
import type { ReconstructResult } from "../state/resume.ts";
|
|
6
4
|
|
|
7
5
|
export interface Sampling {
|
|
8
6
|
readonly maxTokens?: number;
|
|
@@ -10,17 +8,6 @@ export interface Sampling {
|
|
|
10
8
|
readonly reasoning?: ThinkingLevel;
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
export interface RunLogConfig {
|
|
14
|
-
/** Default: true — always-on, opt-out. */
|
|
15
|
-
readonly enabled?: boolean;
|
|
16
|
-
/** Default: ".rlm/runs". Directory under cwd for run artifacts. */
|
|
17
|
-
readonly dir?: string;
|
|
18
|
-
/** Default: true — whether to write sandbox.pkl snapshots. */
|
|
19
|
-
readonly snapshot?: boolean;
|
|
20
|
-
/** Default: 50 — prune oldest runs beyond this count on each new run. */
|
|
21
|
-
readonly maxRuns?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
11
|
export interface RlmConfig {
|
|
25
12
|
/** Persistent editor-routing mode; when enabled, plain interactive prompts use RLM. */
|
|
26
13
|
readonly enabled: boolean;
|
|
@@ -34,10 +21,11 @@ export interface RlmConfig {
|
|
|
34
21
|
readonly requestTimeoutMs: number;
|
|
35
22
|
/** Concurrency pool for *_batched sub-calls. */
|
|
36
23
|
readonly maxConcurrentSubcalls: number;
|
|
24
|
+
/** Concurrent recursive child engines admitted per depth. Lower than maxConcurrentSubcalls:
|
|
25
|
+
* each child is a Python subprocess holding its own copy of the inherited context. */
|
|
26
|
+
readonly maxConcurrentChildren: number;
|
|
37
27
|
/** Reject sub-LLM prompts larger than this many chars. */
|
|
38
28
|
readonly maxPromptChars: number;
|
|
39
|
-
/** Max USD spend across the whole tree before the engine stops (undefined = no cap). */
|
|
40
|
-
readonly maxBudgetUsd?: number;
|
|
41
29
|
/** Max wall-clock ms across the whole tree before the engine stops (undefined = no cap). */
|
|
42
30
|
readonly maxTimeoutMs?: number;
|
|
43
31
|
/** Max total input+output tokens across the whole tree before the engine stops (undefined = no cap). */
|
|
@@ -46,10 +34,6 @@ export interface RlmConfig {
|
|
|
46
34
|
readonly maxErrors?: number;
|
|
47
35
|
/** Append the orchestrator addendum to the system prompt. */
|
|
48
36
|
readonly orchestrator: boolean;
|
|
49
|
-
/** Enable the phase pipeline (advance_phase + stall nags) at depth 0. */
|
|
50
|
-
readonly pipeline: boolean;
|
|
51
|
-
/** Max validate→blueprint corrective re-entries when validation reports blockers (default 2). */
|
|
52
|
-
readonly maxBackwardJumps: number;
|
|
53
37
|
/** Summarize the trajectory when it grows past the threshold (keeps the root window small). */
|
|
54
38
|
readonly compaction: boolean;
|
|
55
39
|
/** Compact when estimated history tokens reach this fraction of the model's context window. */
|
|
@@ -58,12 +42,13 @@ export interface RlmConfig {
|
|
|
58
42
|
readonly python: string;
|
|
59
43
|
/** Worker startup wait before treating sandbox init as failed (ms). */
|
|
60
44
|
readonly sandboxInitTimeoutMs: number;
|
|
61
|
-
/**
|
|
62
|
-
readonly
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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;
|
|
67
52
|
/** ThinkingLevel for the root smart model (set via /rlm-config). */
|
|
68
53
|
readonly smartReasoning?: ThinkingLevel;
|
|
69
54
|
/** Output token cap + temperature for the root smart model per turn.
|
|
@@ -76,8 +61,6 @@ export interface RlmConfig {
|
|
|
76
61
|
readonly subSystemPrompt?: string;
|
|
77
62
|
/** Sampling for sub-LLM (worker) calls. */
|
|
78
63
|
readonly subSampling: Readonly<Sampling>;
|
|
79
|
-
/** Optional run-state persistence configuration. Enabled by default. */
|
|
80
|
-
readonly runLog?: RunLogConfig;
|
|
81
64
|
}
|
|
82
65
|
|
|
83
66
|
/** Input to a (headless) RLM run. */
|
|
@@ -92,12 +75,8 @@ export interface RlmInput {
|
|
|
92
75
|
readonly parentNodeId?: string;
|
|
93
76
|
/** "provider/id" — overrides the root model for this run (set by recursive rlm_query). */
|
|
94
77
|
readonly modelOverride?: string;
|
|
95
|
-
/** Remaining budget for this subtree (set by parent from its LimitGuard). */
|
|
96
|
-
readonly remainingBudgetUsd?: number;
|
|
97
78
|
/** Remaining timeout for this subtree (set by parent from its LimitGuard). */
|
|
98
79
|
readonly remainingTimeoutMs?: number;
|
|
99
|
-
/** Depth-0 resume payload — controller rebuilds this from the trail's `reconstructRlmState()`. */
|
|
100
|
-
readonly resume?: ReconstructResult & { readonly ok: true };
|
|
101
80
|
}
|
|
102
81
|
|
|
103
82
|
/** Result of a completed RLM run. */
|
|
@@ -111,11 +90,4 @@ export interface RlmResult {
|
|
|
111
90
|
}
|
|
112
91
|
|
|
113
92
|
/** A function that runs an RLM to completion — used to wire recursion (rlm_query). */
|
|
114
|
-
export interface InteractiveDeps {
|
|
115
|
-
/** Called when the sandbox issues ask_user_question; undefined = feature disabled. */
|
|
116
|
-
readonly onAskUserQuestion?: (questions: readonly AskQuestion[]) => Promise<AskAnswer[]>;
|
|
117
|
-
/** Called when the sandbox issues todo; undefined = feature disabled. */
|
|
118
|
-
readonly onTodo?: (action: string, params: Record<string, unknown>) => Promise<string>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
93
|
export type RunRlm = (input: RlmInput) => Promise<RlmResult>;
|
package/src/index.ts
CHANGED
|
@@ -7,15 +7,19 @@ import { registerRlmConfigCommand } from "./commands/rlm-config.ts";
|
|
|
7
7
|
import { createRlmTool } from "./tool/rlm-tool.ts";
|
|
8
8
|
import { createReplTool } from "./tool/repl-tool.ts";
|
|
9
9
|
import { loadSettings, mergeConfig, resolveModelId } from "./config/settings.ts";
|
|
10
|
-
import { RlmController
|
|
10
|
+
import { RlmController } from "./mode/rlm-mode.ts";
|
|
11
|
+
import { cheapestModel } from "./mode/llm-model.ts";
|
|
11
12
|
import { postRlmGuide } from "./ui/intro.ts";
|
|
12
13
|
import { setRlmModeStatus } from "./ui/status.ts";
|
|
13
14
|
import { markdownTheme } from "./ui/theme-adapter.ts";
|
|
14
15
|
import { SandboxManager } from "./sandbox/sandbox-manager.ts";
|
|
15
16
|
import { createSubcallGates } from "./util/concurrency.ts";
|
|
16
17
|
import { BackgroundTasks } from "./tool/background-tasks.ts";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
18
|
+
import { resolve } from "node:path";
|
|
19
|
+
import { resolveSource } from "./context/resolve.ts";
|
|
20
|
+
import { formatContextListing } from "./context/listing.ts";
|
|
21
|
+
import type { AddContextHandlerBundle } from "./bridge/add-context.ts";
|
|
22
|
+
import { buildNativeSystemPrompt, NATIVE_TURN_REMINDER } from "./prompts/native.ts";
|
|
19
23
|
import { bashCommandFromInput, isFileReadingCommand, capToolResultText, BASH_BLOCK_REASON } from "./mode/native-guards.ts";
|
|
20
24
|
import { errorMessage } from "./util/errors.ts";
|
|
21
25
|
|
|
@@ -42,9 +46,8 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
42
46
|
});
|
|
43
47
|
// One admission gate for the whole session: spawn() lets the sandbox put many requests on
|
|
44
48
|
// the wire at once, so nothing smaller than session scope actually bounds fan-out.
|
|
45
|
-
const gates = createSubcallGates(config.maxConcurrentSubcalls);
|
|
49
|
+
const gates = createSubcallGates(config.maxConcurrentSubcalls, config.maxConcurrentChildren);
|
|
46
50
|
const background = new BackgroundTasks({
|
|
47
|
-
maxBudgetUsd: config.maxBudgetUsd,
|
|
48
51
|
maxTimeoutMs: config.maxTimeoutMs,
|
|
49
52
|
maxTokens: config.maxTokens,
|
|
50
53
|
maxErrors: config.maxErrors,
|
|
@@ -57,29 +60,44 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
57
60
|
}, WATCHDOG_HEARTBEAT_MS);
|
|
58
61
|
watchdogHeartbeat.unref();
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
/** Memoised cwd seed — one resolveSource(pathPrefix:"") per session. */
|
|
64
|
+
let seedPromise: Promise<void> | undefined;
|
|
65
|
+
let cwdSeeded = false;
|
|
66
|
+
/** Live add_context bundle — seed plants the "" sentinel here so add_context(".") is a no-op. */
|
|
67
|
+
let contextBundleRef: AddContextHandlerBundle | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Payload identity last injected into the context hook. Re-inject only when the payload
|
|
70
|
+
* reference changes (seed, add_context, reset) — not every turn. Keeps the root window small.
|
|
71
|
+
*/
|
|
72
|
+
let listingPayloadRef: unknown = undefined;
|
|
73
|
+
let listingInjected = false;
|
|
74
|
+
const seedContext = async (cwd: string): Promise<void> => {
|
|
75
|
+
if (cwdSeeded) return;
|
|
76
|
+
if (!controller.config.autoSeedCwd) {
|
|
77
|
+
cwdSeeded = true;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
seedPromise ??= resolveSource(cwd, { cwd, pathPrefix: "" })
|
|
65
81
|
.then((result) => {
|
|
82
|
+
// Sticky either way: a failed seed must not re-walk the whole repo on every repl().
|
|
83
|
+
cwdSeeded = true;
|
|
66
84
|
if (!result.ok) {
|
|
67
|
-
console.warn(`[rlm]
|
|
68
|
-
return
|
|
85
|
+
console.warn(`[rlm] context seed failed: ${result.error}`);
|
|
86
|
+
return;
|
|
69
87
|
}
|
|
70
|
-
sandboxManager.contextPayload =
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
sandboxManager.contextPayload = result.value.payload;
|
|
89
|
+
// Register the cwd seed with the bridge so add_context(".") cannot double the tree.
|
|
90
|
+
contextBundleRef?.markSeededCwd(resolve(cwd));
|
|
73
91
|
})
|
|
74
|
-
.finally(() => {
|
|
75
|
-
|
|
92
|
+
.finally(() => { seedPromise = undefined; });
|
|
93
|
+
await seedPromise;
|
|
76
94
|
};
|
|
77
95
|
|
|
78
96
|
// Load persisted settings async — applied before session_start handler reads controller state
|
|
79
97
|
const settingsReady = loadSettings()
|
|
80
98
|
.then((persisted) => {
|
|
81
99
|
controller.config = mergeConfig(persisted.config);
|
|
82
|
-
controller.
|
|
100
|
+
controller.savedLlmRef = persisted.llm;
|
|
83
101
|
})
|
|
84
102
|
.catch((err) => {
|
|
85
103
|
console.warn(`[rlm] settings load failed: ${errorMessage(err)}`);
|
|
@@ -120,30 +138,48 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
120
138
|
const flag = pi.getFlag("rlm");
|
|
121
139
|
if (typeof flag === "boolean") controller.setConfig(Object.freeze({ ...controller.config, enabled: flag }));
|
|
122
140
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
// Reload the catalog before the worker-model pick below reads it. Newer pi builds make
|
|
142
|
+
// `getAvailable()` an async-populated snapshot that starts empty, and picking from an empty
|
|
143
|
+
// catalog silently falls back to the root model. Called with no arguments and awaited so it
|
|
144
|
+
// is valid whether `refresh` returns void (current) or a promise (newer); fail-soft, because
|
|
145
|
+
// a refresh error must not abort session start.
|
|
146
|
+
try {
|
|
147
|
+
await ctx.modelRegistry.refresh();
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.warn(`[rlm] model registry refresh failed: ${errorMessage(err)}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (controller.savedLlmRef) {
|
|
153
|
+
const resolved = resolveModelId(ctx.modelRegistry, controller.savedLlmRef);
|
|
154
|
+
if (resolved) controller.llmModel = resolved;
|
|
126
155
|
}
|
|
127
156
|
|
|
128
157
|
// Re-register repl tool each session to pick up model provider changes
|
|
129
|
-
const
|
|
158
|
+
const llmModel = controller.llmModel ?? cheapestModel(ctx.modelRegistry) ?? ctx.model;
|
|
130
159
|
const model = ctx.model;
|
|
131
|
-
if (
|
|
160
|
+
if (llmModel && model) {
|
|
132
161
|
try {
|
|
133
162
|
pi.registerTool(createReplTool({
|
|
134
163
|
sandboxManager,
|
|
135
164
|
model,
|
|
136
|
-
|
|
165
|
+
llmModel,
|
|
137
166
|
getModel: () => controller.resolveModels(ctx)?.model,
|
|
138
|
-
|
|
167
|
+
getLlmModel: () => controller.resolveModels(ctx)?.llm,
|
|
139
168
|
registry: ctx.modelRegistry,
|
|
140
169
|
getConfig: () => controller.config,
|
|
141
170
|
gates,
|
|
142
171
|
background,
|
|
143
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
|
+
},
|
|
144
181
|
ensureContext: async () => {
|
|
145
|
-
|
|
146
|
-
if (contextText === undefined) throw new Error("repository context could not be loaded into RLM sandbox");
|
|
182
|
+
await seedContext(ctx.cwd ?? process.cwd());
|
|
147
183
|
},
|
|
148
184
|
}));
|
|
149
185
|
} catch (err) {
|
|
@@ -173,9 +209,10 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
173
209
|
return { systemPrompt: event.systemPrompt + "\n\n" + buildNativeSystemPrompt() };
|
|
174
210
|
});
|
|
175
211
|
|
|
176
|
-
// ── Context injection:
|
|
177
|
-
|
|
178
|
-
|
|
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) => {
|
|
179
216
|
const filtered = event.messages.filter(
|
|
180
217
|
(message) =>
|
|
181
218
|
!(message.role === "custom" && message.customType === "rlm-intro")
|
|
@@ -185,25 +222,24 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
185
222
|
|
|
186
223
|
type PiMessage = (typeof filtered)[number];
|
|
187
224
|
|
|
188
|
-
|
|
189
|
-
if (!
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
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);
|
|
207
243
|
}
|
|
208
244
|
|
|
209
245
|
// Per-turn last-position reminder (not persisted — context hook rebuilds every request)
|
|
@@ -225,7 +261,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
225
261
|
if (BLOCKED_NATIVE_TOOLS.has(event.toolName)) {
|
|
226
262
|
return {
|
|
227
263
|
block: true,
|
|
228
|
-
reason: "RLM mode active. Use repl({code}) to read files and search the repository —
|
|
264
|
+
reason: "RLM mode active. Use repl({code}) to read files and search the repository — loaded files live in the REPL `context` variable (cwd seeds on first call). Use `edit`/`write` for file changes. If sub-LLM credits are exhausted, report to the user.",
|
|
229
265
|
};
|
|
230
266
|
}
|
|
231
267
|
const bashCommand = event.toolName === "bash" ? bashCommandFromInput(event.input) : undefined;
|
|
@@ -253,9 +289,11 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
253
289
|
clearInterval(watchdogHeartbeat);
|
|
254
290
|
background.dispose();
|
|
255
291
|
await sandboxManager.dispose();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
292
|
+
cwdSeeded = false;
|
|
293
|
+
seedPromise = undefined;
|
|
294
|
+
contextBundleRef = undefined;
|
|
295
|
+
listingPayloadRef = undefined;
|
|
296
|
+
listingInjected = false;
|
|
297
|
+
sandboxManager.contextPayload = [];
|
|
260
298
|
});
|
|
261
299
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-LLM model ranking — "cheapest available", with free models winning outright.
|
|
3
|
+
*
|
|
4
|
+
* Pi's `ModelCost` is non-nullable (`packages/ai/src/types.ts`), so a free model is a literal 0,
|
|
5
|
+
* not a null. That makes plain price sorting ambiguous rather than wrong: subscription and
|
|
6
|
+
* token-plan providers also publish 0, and a stable sort would hand back whichever 0-cost entry
|
|
7
|
+
* happened to be first in catalog order. The tie-breaks below are what actually pick a usable
|
|
8
|
+
* free model — and what make the pick identical across sessions and catalog reorderings.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
12
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
/** $/Mtok, input-weighted 3:1 — a sub-call sends a file body and gets back a sentence. */
|
|
15
|
+
function priceOf(model: Model<Api>): number {
|
|
16
|
+
const { input, output, cacheRead } = model.cost;
|
|
17
|
+
return input * 3 + output + cacheRead;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** True when the model costs nothing to call on any axis. */
|
|
21
|
+
export function isFreeModel(model: Model<Api>): boolean {
|
|
22
|
+
return priceOf(model) === 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Negative when `a` is the better sub-LLM.
|
|
27
|
+
*
|
|
28
|
+
* Window before maxTokens before id: a free model with a 4K context is useless for bulk reading,
|
|
29
|
+
* so price alone must not decide. The final id comparison exists only to make the result
|
|
30
|
+
* deterministic — without it the pick drifts whenever a provider reorders its catalog.
|
|
31
|
+
*/
|
|
32
|
+
export function compareLlm(a: Model<Api>, b: Model<Api>): number {
|
|
33
|
+
return (priceOf(a) - priceOf(b))
|
|
34
|
+
|| (b.contextWindow - a.contextWindow)
|
|
35
|
+
|| (b.maxTokens - a.maxTokens)
|
|
36
|
+
|| `${a.provider}/${a.id}`.localeCompare(`${b.provider}/${b.id}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Best sub-LLM among the models whose provider has configured auth.
|
|
41
|
+
*
|
|
42
|
+
* Single pass rather than `[...models].sort()[0]`: the copy and the sort both allocate for a
|
|
43
|
+
* result that is one element.
|
|
44
|
+
*/
|
|
45
|
+
export function cheapestModel(registry: ModelRegistry): Model<Api> | undefined {
|
|
46
|
+
const models = registry.getAvailable();
|
|
47
|
+
let best: Model<Api> | undefined;
|
|
48
|
+
for (let i = 0; i < models.length; i++) {
|
|
49
|
+
const model = models[i];
|
|
50
|
+
if (model === undefined) continue;
|
|
51
|
+
if (best === undefined || compareLlm(model, best) < 0) best = model;
|
|
52
|
+
}
|
|
53
|
+
return best;
|
|
54
|
+
}
|
package/src/mode/rlm-mode.ts
CHANGED
|
@@ -1,42 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RlmController — holds RLM config + chosen models.
|
|
3
3
|
*
|
|
4
|
-
* The engine drives the root model turn-by-turn over ```repl``` blocks with full
|
|
4
|
+
* The engine drives the root model turn-by-turn over ```repl``` blocks with full token/
|
|
5
5
|
* timeout/error guards, compaction, and a finalize fallback. `start()` returns a RunHandle with
|
|
6
6
|
* the completion promise.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
10
|
-
import type { ExtensionContext
|
|
11
|
-
import { DEFAULT_RUN_DIR } from "../config/defaults.ts";
|
|
10
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
11
|
import { modelRef, resolveModelId, saveSettings } from "../config/settings.ts";
|
|
13
12
|
import { createEngine } from "../core/engine.ts";
|
|
14
13
|
import { limitsFromConfig } from "../core/limits.ts";
|
|
15
|
-
import type {
|
|
16
|
-
import
|
|
17
|
-
import { packRepository, serializeForSandbox } from "../context/repomix-context.ts";
|
|
14
|
+
import type { RlmConfig, RlmResult } from "../core/types.ts";
|
|
15
|
+
import { resolveSource } from "../context/resolve.ts";
|
|
18
16
|
import { RlmEmitter } from "../tool/rlm-events.ts";
|
|
19
17
|
import { formatError } from "../util/errors.ts";
|
|
20
|
-
|
|
21
|
-
export function cheapestModel(registry: ModelRegistry): Model<Api> | undefined {
|
|
22
|
-
const models = registry.getAvailable();
|
|
23
|
-
if (models.length === 0) return undefined;
|
|
24
|
-
return [...models].sort((a, b) => a.cost.input + a.cost.output - (b.cost.input + b.cost.output))[0];
|
|
25
|
-
}
|
|
18
|
+
import { cheapestModel } from "./llm-model.ts";
|
|
26
19
|
|
|
27
20
|
export interface RunHandle {
|
|
28
21
|
readonly abort: () => void;
|
|
29
22
|
readonly done: Promise<RlmResult>;
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
export interface StartInput {
|
|
26
|
+
readonly rootPrompt: string;
|
|
27
|
+
readonly context: unknown;
|
|
28
|
+
}
|
|
36
29
|
|
|
37
30
|
export class RlmController {
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
llmModel: Model<Api> | undefined;
|
|
32
|
+
savedLlmRef: string | undefined;
|
|
40
33
|
private active: AbortController | null = null;
|
|
41
34
|
|
|
42
35
|
constructor(public config: RlmConfig) {}
|
|
@@ -65,7 +58,7 @@ export class RlmController {
|
|
|
65
58
|
async persist(): Promise<boolean> {
|
|
66
59
|
return await saveSettings({
|
|
67
60
|
config: this.config,
|
|
68
|
-
|
|
61
|
+
llm: modelRef(this.llmModel) ?? this.savedLlmRef,
|
|
69
62
|
});
|
|
70
63
|
}
|
|
71
64
|
|
|
@@ -77,15 +70,15 @@ export class RlmController {
|
|
|
77
70
|
this.active?.abort();
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
resolveModels(ctx: ExtensionContext): { model: Model<Api>;
|
|
81
|
-
if (!this.
|
|
73
|
+
resolveModels(ctx: ExtensionContext): { model: Model<Api>; llm: Model<Api> } | undefined {
|
|
74
|
+
if (!this.llmModel && this.savedLlmRef) this.llmModel = resolveModelId(ctx.modelRegistry, this.savedLlmRef);
|
|
82
75
|
const model = ctx.model ?? cheapestModel(ctx.modelRegistry);
|
|
83
76
|
if (!model) return undefined;
|
|
84
|
-
const
|
|
85
|
-
return { model,
|
|
77
|
+
const llm = this.llmModel ?? cheapestModel(ctx.modelRegistry) ?? model;
|
|
78
|
+
return { model, llm };
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
start(ctx: ExtensionContext, input: StartInput, emitter?: RlmEmitter
|
|
81
|
+
start(ctx: ExtensionContext, input: StartInput, emitter?: RlmEmitter): RunHandle {
|
|
89
82
|
const models = this.resolveModels(ctx);
|
|
90
83
|
if (!models) throw new Error("no model with configured auth is available");
|
|
91
84
|
if (this.active) throw new Error("RLM run already in progress"); // QC: mutual-exclusion guard
|
|
@@ -93,50 +86,27 @@ export class RlmController {
|
|
|
93
86
|
const abortController = new AbortController();
|
|
94
87
|
this.active = abortController;
|
|
95
88
|
|
|
96
|
-
const runState = this.config.runLog?.enabled !== false
|
|
97
|
-
? { cwd: ctx.cwd ?? process.cwd(), dir: this.config.runLog?.dir ?? DEFAULT_RUN_DIR, snapshot: this.config.runLog?.snapshot !== false }
|
|
98
|
-
: undefined;
|
|
99
|
-
|
|
100
89
|
const done = (async () => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} else {
|
|
111
|
-
contextValue = formatError(`failed to pack repository — ${result.error}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
engineInput = {
|
|
115
|
-
rootPrompt: input.rootPrompt,
|
|
116
|
-
context: contextValue,
|
|
117
|
-
depth: 0,
|
|
118
|
-
};
|
|
119
|
-
} else {
|
|
120
|
-
engineInput = {
|
|
121
|
-
rootPrompt: input.resume.header.rootPrompt,
|
|
122
|
-
context: input.context, // B5: load the actual context from the sidecar, not ""
|
|
123
|
-
depth: 0,
|
|
124
|
-
resume: input.resume,
|
|
125
|
-
};
|
|
90
|
+
// Auto-seed empty/undefined context from cwd (same resolveSource path as native mode);
|
|
91
|
+
// pass explicit context through.
|
|
92
|
+
let contextValue: unknown = input.context;
|
|
93
|
+
if (contextValue === undefined || (typeof contextValue === "string" && contextValue.trim() === "")) {
|
|
94
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
95
|
+
const result = await resolveSource(cwd, { cwd, pathPrefix: "", signal: abortController.signal });
|
|
96
|
+
contextValue = result.ok
|
|
97
|
+
? result.value.payload
|
|
98
|
+
: formatError(`failed to pack repository — ${result.error}`);
|
|
126
99
|
}
|
|
127
100
|
const engine = createEngine({
|
|
128
101
|
model: models.model,
|
|
129
|
-
|
|
102
|
+
llmModel: models.llm,
|
|
130
103
|
registry: ctx.modelRegistry,
|
|
131
104
|
config: this.config,
|
|
132
105
|
signal: abortController.signal,
|
|
133
106
|
emitter: emitter ?? new RlmEmitter(),
|
|
134
|
-
runState,
|
|
135
|
-
onAskUserQuestion: interactive?.onAskUserQuestion,
|
|
136
|
-
onTodo: interactive?.onTodo,
|
|
137
107
|
limits: limitsFromConfig(this.config),
|
|
138
108
|
});
|
|
139
|
-
return await engine(
|
|
109
|
+
return await engine({ rootPrompt: input.rootPrompt, context: contextValue, depth: 0 });
|
|
140
110
|
})().finally(() => {
|
|
141
111
|
if (this.active === abortController) this.active = null;
|
|
142
112
|
});
|