@hicaru/pi-rlm 0.1.8 → 0.2.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 +22 -19
- package/package.json +2 -1
- package/src/bridge/library.ts +93 -15
- package/src/bridge/llm-query.ts +60 -36
- package/src/bridge/rlm-query.ts +63 -79
- package/src/commands/rlm-config.ts +8 -8
- package/src/commands/rlm.ts +48 -12
- package/src/config/settings.ts +33 -3
- package/src/context/library-context.ts +209 -22
- package/src/context/repomix-context.ts +7 -58
- package/src/core/answer.ts +5 -13
- package/src/core/artifacts.ts +4 -3
- package/src/core/critique.ts +92 -0
- package/src/core/engine.ts +94 -299
- package/src/core/gates.ts +33 -4
- package/src/core/limits.ts +19 -1
- package/src/core/pipeline-handlers.ts +319 -0
- package/src/core/pipeline.ts +40 -15
- package/src/core/types.ts +26 -30
- package/src/index.ts +36 -26
- package/src/mode/native-guards.ts +2 -2
- package/src/mode/rlm-mode.ts +8 -11
- package/src/prompts/phases.ts +18 -39
- package/src/prompts/system.ts +167 -64
- package/src/prompts/user.ts +1 -5
- package/src/sandbox/protocol.ts +5 -17
- package/src/sandbox/sandbox-manager.ts +5 -5
- package/src/sandbox/sandbox.ts +67 -27
- package/src/sandbox/worker.py +534 -48
- package/src/state/paths.ts +1 -1
- package/src/state/reads.ts +12 -4
- package/src/state/resume.ts +26 -25
- package/src/state/rows.ts +2 -2
- package/src/text/parsing.ts +0 -6
- package/src/text/tokens.ts +7 -1
- package/src/tool/repl-details.ts +2 -3
- package/src/tool/repl-tool.ts +132 -337
- package/src/tool/rlm-aggregator.ts +7 -7
- package/src/tool/rlm-details.ts +6 -13
- package/src/tool/rlm-events.ts +14 -11
- package/src/tool/rlm-tool.ts +20 -38
- package/src/tool/subcall-render.ts +61 -9
- package/src/tool/subcall-store.ts +4 -2
- package/src/ui/config-panel.ts +43 -23
- package/src/ui/intro.ts +2 -1
- package/src/ui/status.ts +8 -5
- package/src/ui/theme-adapter.ts +36 -0
- package/src/ui/theme.ts +0 -25
- package/src/mode/input-router.ts +0 -23
- package/src/registry/edit-registry.ts +0 -22
- package/src/text/edits.ts +0 -164
- package/src/tool/apply-edits-tool.ts +0 -295
package/src/tool/repl-tool.ts
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* and collects sub-calls manually from emitter events. No RlmEventAggregator is used
|
|
7
7
|
* (ReplDetails ≠ RlmDetails structural mismatch).
|
|
8
8
|
*
|
|
9
|
-
* Sandbox handlers (llm_query, rlm_query, todo, ask_user_question)
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
9
|
+
* Sandbox handlers (llm_query, rlm_query, todo, ask_user_question) are the *shared* bridges
|
|
10
|
+
* from bridge/llm-query.ts and bridge/rlm-query.ts, bound to NativeBridgeState accessors so
|
|
11
|
+
* the tool can swap per-invocation state (emitter, depth, limits) without recreating the
|
|
12
|
+
* sandbox — preserving REPL variable state across calls.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { Type } from "typebox";
|
|
@@ -16,103 +17,91 @@ import type { Theme, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
|
16
17
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
17
18
|
import type { Model, Usage, Api } from "@earendil-works/pi-ai";
|
|
18
19
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import {
|
|
20
|
+
import { displayModelRef } from "../config/settings.ts";
|
|
20
21
|
import { buildInteractiveHandlers } from "../bridge/interactive.ts";
|
|
21
22
|
import { buildLibraryHandler } from "../bridge/library.ts";
|
|
22
23
|
import { createPiInteractiveDeps } from "../bridge/pi-interactive.ts";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import type { InteractiveDeps, RlmConfig, Sampling } from "../core/types.ts";
|
|
24
|
+
import { createLlmBridge } from "../bridge/llm-query.ts";
|
|
25
|
+
import { createRlmHandlers } from "../bridge/rlm-query.ts";
|
|
26
|
+
import { LimitGuard, limitsFromConfig } from "../core/limits.ts";
|
|
27
|
+
import type { RemainingResources } from "../core/resource-limits.ts";
|
|
28
|
+
import type { InteractiveDeps, RlmConfig, RunRlm } from "../core/types.ts";
|
|
29
29
|
import { SandboxManager } from "../sandbox/sandbox-manager.ts";
|
|
30
|
-
import type {
|
|
31
|
-
import type { ProposedEdit, ReplResult } from "../sandbox/protocol.ts";
|
|
30
|
+
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
32
31
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
33
32
|
import { SubcallStore } from "./subcall-store.ts";
|
|
34
33
|
import type { ReplDetails } from "./repl-details.ts";
|
|
35
34
|
import type { RlmSubcall } from "./rlm-details.ts";
|
|
36
35
|
import { createEngine } from "../core/engine.ts";
|
|
37
|
-
import {
|
|
38
|
-
import
|
|
39
|
-
import { errorMessage
|
|
36
|
+
import { spinnerFrame } from "../ui/theme.ts";
|
|
37
|
+
import { previewText } from "../text/preview.ts";
|
|
38
|
+
import { errorMessage } from "../util/errors.ts";
|
|
40
39
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
cardHeader,
|
|
41
|
+
cardStatsLine,
|
|
42
|
+
renderCollapsedCard,
|
|
43
43
|
renderExpandedSubcallTree,
|
|
44
44
|
} from "./subcall-render.ts";
|
|
45
45
|
import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
|
|
46
46
|
import { capReplResultText, replDelegationNudge } from "../mode/native-guards.ts";
|
|
47
47
|
|
|
48
|
+
/** Chars of code shown on the tool call line, and of stdout in the expanded view. */
|
|
49
|
+
const CALL_PREVIEW_CHARS = 80;
|
|
50
|
+
const EXPANDED_STDOUT_CHARS = 2_000;
|
|
51
|
+
const EXPANDED_STDERR_CHARS = 500;
|
|
52
|
+
|
|
48
53
|
// ── Parameter schema ──
|
|
49
54
|
|
|
50
55
|
export const ReplToolParams = Object.freeze(Type.Object({
|
|
51
56
|
code: Type.String({ description: "Python code to execute in the persistent REPL sandbox" }),
|
|
52
57
|
}));
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
return edits.length > 0 && !raised ? edits : undefined;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Model-visible text assembled from a repl() result, plus the surfaced edits for `details`. */
|
|
59
|
+
/** Model-visible text assembled from a repl() result. */
|
|
59
60
|
export interface ReplResultText {
|
|
60
61
|
readonly text: string;
|
|
61
|
-
readonly surfacedEdits: readonly ProposedEdit[] | undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function countLines(text: string): number {
|
|
65
|
-
if (text.length === 0) return 0;
|
|
66
|
-
let count = 1;
|
|
67
|
-
for (const ch of text) if (ch === "\n") count++;
|
|
68
|
-
return count;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function stagedEditSummary(edits: readonly ProposedEdit[]): string {
|
|
72
|
-
const rows = new Array<string>(edits.length);
|
|
73
|
-
for (let i = 0; i < edits.length; i++) {
|
|
74
|
-
const edit = edits[i];
|
|
75
|
-
rows[i] = ` ${edit.id} ${edit.path} (-${countLines(edit.oldText)}/+${countLines(edit.newText)} lines)`;
|
|
76
|
-
}
|
|
77
|
-
return [
|
|
78
|
-
"STAGED_EDITS (apply by id with apply_edits; do NOT re-type content):",
|
|
79
|
-
...rows,
|
|
80
|
-
].join("\n");
|
|
81
62
|
}
|
|
82
63
|
|
|
83
64
|
/**
|
|
84
|
-
* Assemble the model-visible text for a repl() result: cap stdout
|
|
85
|
-
* delegation nudge
|
|
86
|
-
* without exposing oldText/newText bodies to the root model.
|
|
65
|
+
* Assemble the model-visible text for a repl() result: cap stdout and append a
|
|
66
|
+
* zero-subcall delegation nudge when a bulk read went undelegated.
|
|
87
67
|
*/
|
|
88
68
|
export function buildReplResultText(
|
|
89
69
|
stdout: string,
|
|
90
70
|
finalAnswer: string | undefined,
|
|
91
|
-
edits: readonly ProposedEdit[],
|
|
92
|
-
raised: boolean,
|
|
93
71
|
subcalls: readonly RlmSubcall[],
|
|
94
72
|
): ReplResultText {
|
|
95
73
|
const answerSubmitted = finalAnswer !== undefined;
|
|
96
74
|
const rawText = answerSubmitted
|
|
97
75
|
? `ANSWER_SUBMITTED (${finalAnswer.length} chars) — delivered to user. Do not restate it.`
|
|
98
76
|
: stdout || "(no output)";
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const modelText = rawText + editsBlock;
|
|
102
|
-
// Model-visible text is capped; the caller keeps full stdout/final answer in `details` for the TUI.
|
|
103
|
-
const cappedText = capReplResultText(modelText) ?? modelText;
|
|
77
|
+
// Model-visible text is capped; the caller keeps full stdout in `details` for the TUI.
|
|
78
|
+
const cappedText = capReplResultText(rawText) ?? rawText;
|
|
104
79
|
const delegated = subcalls.some((s) => s.kind === "llm" || s.kind === "batch" || s.kind === "rlm");
|
|
105
|
-
const nudge =
|
|
106
|
-
return { text: cappedText + (nudge ?? "")
|
|
80
|
+
const nudge = answerSubmitted ? undefined : replDelegationNudge(rawText.length, delegated);
|
|
81
|
+
return { text: cappedText + (nudge ?? "") };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Advisory diagnostics derived from a completed invocation's sub-calls. */
|
|
85
|
+
export function collectReplWarnings(subcalls: readonly RlmSubcall[]): readonly string[] | undefined {
|
|
86
|
+
let failed = 0;
|
|
87
|
+
let total = 0;
|
|
88
|
+
for (let i = 0; i < subcalls.length; i++) {
|
|
89
|
+
const call = subcalls[i];
|
|
90
|
+
if (call.status !== "error") continue;
|
|
91
|
+
// A batch subcall stands for many prompts; a single call stands for one.
|
|
92
|
+
failed += call.failedCount ?? 1;
|
|
93
|
+
total += call.totalCount ?? 1;
|
|
94
|
+
}
|
|
95
|
+
if (failed === 0) return undefined;
|
|
96
|
+
return Object.freeze([`${failed}/${total} sub-call(s) failed — results may be incomplete`]);
|
|
107
97
|
}
|
|
108
98
|
|
|
109
99
|
// ── Mutable bridge state (handler indirection) ──
|
|
110
100
|
|
|
111
101
|
/**
|
|
112
|
-
* Holds per-invocation mutable state that
|
|
113
|
-
* The sandbox is created once with handlers
|
|
114
|
-
*
|
|
115
|
-
* without recreating the sandbox (preserving REPL variable state).
|
|
102
|
+
* Holds per-invocation mutable state that the shared bridges dereference through accessors.
|
|
103
|
+
* The sandbox is created once with handlers bound to this object, so the tool can swap
|
|
104
|
+
* emitter/depth/limits between calls without recreating the sandbox (preserving REPL state).
|
|
116
105
|
*/
|
|
117
106
|
class NativeBridgeState {
|
|
118
107
|
currentEmitter: RlmEmitter | null = null;
|
|
@@ -129,202 +118,11 @@ class NativeBridgeState {
|
|
|
129
118
|
this.currentInteractive = inv.interactive;
|
|
130
119
|
}
|
|
131
120
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
maxConcurrent: number;
|
|
138
|
-
sampling?: Sampling;
|
|
139
|
-
subSystem?: string;
|
|
140
|
-
signal?: AbortSignal;
|
|
141
|
-
}): Pick<SubLlmHandlers, "llmQuery" | "llmQueryBatched"> {
|
|
142
|
-
const state = this;
|
|
143
|
-
|
|
144
|
-
const workerModel = (): Model<Api> => deps.getWorkerModel?.() ?? deps.workerModel;
|
|
145
|
-
const displayModel = (model: string | null): string =>
|
|
146
|
-
modelRef(model ? (resolveModelId(deps.registry, model) ?? workerModel()) : workerModel()) ?? workerModel().id;
|
|
147
|
-
|
|
148
|
-
async function complete1(prompt: string, model: string | null, track: (u: Usage) => void): Promise<string> {
|
|
149
|
-
const limits = state.currentLimits;
|
|
150
|
-
if (limits) {
|
|
151
|
-
const limitError = checkResourceLimits({ budgetUsd: limits.remainingBudgetUsd(), timeoutMs: limits.remainingTimeoutMs() });
|
|
152
|
-
if (limitError !== undefined) return limitError;
|
|
153
|
-
}
|
|
154
|
-
if (prompt.length > deps.maxPromptChars) {
|
|
155
|
-
return formatError(`sub-LLM prompt exceeded size limit (${prompt.length.toLocaleString()} chars > ${deps.maxPromptChars.toLocaleString()})`);
|
|
156
|
-
}
|
|
157
|
-
const resolved = model ? resolveModelId(deps.registry, model) : undefined;
|
|
158
|
-
if (model && !resolved) return formatError(`unknown model override '${model}'`);
|
|
159
|
-
try {
|
|
160
|
-
const messages: ChatMsg[] = [{ role: "user", content: prompt }];
|
|
161
|
-
const res = await modelComplete(messages, {
|
|
162
|
-
model: resolved ?? workerModel(),
|
|
163
|
-
registry: deps.registry,
|
|
164
|
-
system: deps.subSystem,
|
|
165
|
-
maxTokens: deps.sampling?.maxTokens,
|
|
166
|
-
temperature: deps.sampling?.temperature,
|
|
167
|
-
reasoning: deps.sampling?.reasoning,
|
|
168
|
-
signal: deps.signal,
|
|
169
|
-
});
|
|
170
|
-
limits?.addUsage(res.usage);
|
|
171
|
-
track(res.usage);
|
|
172
|
-
return res.text;
|
|
173
|
-
} catch (err) {
|
|
174
|
-
const msg = errorMessage(err);
|
|
175
|
-
const hint = /credit|402|payment|quota|rate.limit/i.test(msg)
|
|
176
|
-
? " — try smaller batches or individual llm_query calls"
|
|
177
|
-
: "";
|
|
178
|
-
return formatError(`${msg}${hint}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
async llmQuery(prompt, model, _depth) {
|
|
184
|
-
const id = state.currentEmitter?.emitSubcallCreated({
|
|
185
|
-
kind: "llm", parentId: state.currentParentId, label: "llm_query",
|
|
186
|
-
model: displayModel(model), args: `prompt: ${previewText(prompt)}`,
|
|
187
|
-
depth: state.currentDepth,
|
|
188
|
-
});
|
|
189
|
-
let cost = 0; let tokens = 0;
|
|
190
|
-
const out = await complete1(prompt, model, (u) => { cost += u.cost.total; tokens += u.totalTokens; });
|
|
191
|
-
if (id) state.currentEmitter?.emitSubcallUpdated({ id,
|
|
192
|
-
status: isErrorText(out) ? "error" : "done",
|
|
193
|
-
costUsd: cost, tokens, resultPreview: previewText(out),
|
|
194
|
-
detail: isErrorText(out) ? out : undefined,
|
|
195
|
-
});
|
|
196
|
-
return out;
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
async llmQueryBatched(prompts: readonly string[], model, _depth): Promise<string[]> {
|
|
200
|
-
const id = state.currentEmitter?.emitSubcallCreated({
|
|
201
|
-
kind: "batch", parentId: state.currentParentId, label: `llm_query ×${prompts.length}`,
|
|
202
|
-
model: displayModel(model), args: `prompt: ${previewText(prompts[0] ?? "")}`,
|
|
203
|
-
depth: state.currentDepth,
|
|
204
|
-
});
|
|
205
|
-
let cost = 0; let tokens = 0;
|
|
206
|
-
const out: string[] = await mapPool(prompts, deps.maxConcurrent, (p) =>
|
|
207
|
-
complete1(p, model, (u) => { cost += u.cost.total; tokens += u.totalTokens; }),
|
|
208
|
-
);
|
|
209
|
-
const failed = out.filter(isErrorText).length;
|
|
210
|
-
const allFailed = failed === out.length;
|
|
211
|
-
const error = allFailed
|
|
212
|
-
? `all ${out.length} sub-calls failed — reduce batch size or try llm_query individually`
|
|
213
|
-
: failed > 0 ? `${failed}/${out.length} sub-calls failed` : undefined;
|
|
214
|
-
if (id) state.currentEmitter?.emitSubcallUpdated({ id,
|
|
215
|
-
status: error ? "error" : "done", costUsd: cost, tokens,
|
|
216
|
-
resultPreview: previewText(out[0] ?? ""), detail: error,
|
|
217
|
-
});
|
|
218
|
-
return out;
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Build real recursive rlm_query / rlm_query_batched handlers that spawn
|
|
225
|
-
* child RLM engines (each with its own sandbox and turn loop) rather than
|
|
226
|
-
* falling back to a one-shot llm_query.
|
|
227
|
-
*
|
|
228
|
-
* At the maxDepth cap the handler degrades to a plain llm_query via the
|
|
229
|
-
* already-wired llmHandlers (which read from the same mutable state).
|
|
230
|
-
*/
|
|
231
|
-
buildRlmHandlers(deps: {
|
|
232
|
-
model: Model<Api>;
|
|
233
|
-
workerModel: Model<Api>;
|
|
234
|
-
getModel?: () => Model<Api> | undefined;
|
|
235
|
-
getWorkerModel?: () => Model<Api> | undefined;
|
|
236
|
-
registry: ModelRegistry;
|
|
237
|
-
config: RlmConfig;
|
|
238
|
-
signal?: AbortSignal;
|
|
239
|
-
onUsage?: (usage: Usage, role: "sub") => void;
|
|
240
|
-
llmHandlers: Pick<SubLlmHandlers, "llmQuery" | "llmQueryBatched">;
|
|
241
|
-
}): Pick<SubLlmHandlers, "rlmQuery" | "rlmQueryBatched"> {
|
|
242
|
-
const state = this;
|
|
243
|
-
|
|
244
|
-
async function rlmQueryImpl(prompt: string, model: string | null, depth: number): Promise<string> {
|
|
245
|
-
const emitter = state.currentEmitter;
|
|
246
|
-
const limits = state.currentLimits;
|
|
247
|
-
if (!emitter || !limits) return formatError("RLM bridge not wired for this invocation");
|
|
248
|
-
|
|
249
|
-
const childDepth = state.currentDepth + 1;
|
|
250
|
-
|
|
251
|
-
// Depth cap: degrade to a one-shot llm_query.
|
|
252
|
-
if (childDepth >= deps.config.maxDepth) {
|
|
253
|
-
return deps.llmHandlers.llmQuery(prompt, model, depth);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const remBudget = limits.remainingBudgetUsd();
|
|
257
|
-
const remTimeout = limits.remainingTimeoutMs();
|
|
258
|
-
const limitError = checkResourceLimits({ budgetUsd: remBudget, timeoutMs: remTimeout });
|
|
259
|
-
if (limitError) return limitError;
|
|
260
|
-
|
|
261
|
-
const rootModel = deps.getModel?.() ?? deps.model;
|
|
262
|
-
const workerModel = deps.getWorkerModel?.() ?? deps.workerModel;
|
|
263
|
-
const resolvedOverride = model ? resolveModelId(deps.registry, model) : undefined;
|
|
264
|
-
const subId = emitter.emitSubcallCreated({
|
|
265
|
-
kind: "rlm", parentId: state.currentParentId, label: "rlm_query",
|
|
266
|
-
model: model ? (modelRef(resolvedOverride) ?? `unknown/${model}`) : (modelRef(rootModel) ?? rootModel.id),
|
|
267
|
-
detail: prompt.slice(0, 60),
|
|
268
|
-
depth: childDepth,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// Per-call engine creation with the visible emitter — child llm_query subcalls,
|
|
272
|
-
// turn progress, and cost deltas land on the per-invocation emitter, visible to
|
|
273
|
-
// SubcallStore and the live visual tree.
|
|
274
|
-
const runRlm = createEngine({
|
|
275
|
-
model: rootModel,
|
|
276
|
-
workerModel,
|
|
277
|
-
registry: deps.registry,
|
|
278
|
-
config: deps.config,
|
|
279
|
-
signal: deps.signal,
|
|
280
|
-
emitter: emitter,
|
|
281
|
-
onUsage: deps.onUsage as ((usage: Usage, role: "root" | "sub") => void) | undefined,
|
|
282
|
-
limits: {
|
|
283
|
-
maxBudgetUsd: deps.config.maxBudgetUsd,
|
|
284
|
-
maxTimeoutMs: deps.config.maxTimeoutMs,
|
|
285
|
-
maxTokens: deps.config.maxTokens,
|
|
286
|
-
maxErrors: deps.config.maxErrors,
|
|
287
|
-
},
|
|
288
|
-
onTodo: state.currentInteractive?.onTodo,
|
|
289
|
-
onAskUserQuestion: state.currentInteractive?.onAskUserQuestion,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
const res = await runRlm({
|
|
294
|
-
rootPrompt: "",
|
|
295
|
-
context: prompt,
|
|
296
|
-
depth: childDepth,
|
|
297
|
-
parentNodeId: subId,
|
|
298
|
-
modelOverride: model ?? undefined,
|
|
299
|
-
remainingBudgetUsd: remBudget,
|
|
300
|
-
remainingTimeoutMs: remTimeout,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Debit parent limit guard for the entire child run.
|
|
304
|
-
limits.addRaw(res.costUsd, res.inputTokens, res.outputTokens);
|
|
305
|
-
|
|
306
|
-
// Child engine emits live usage deltas via the shared emitter — SubcallStore
|
|
307
|
-
// accumulates them. No final aggregate costUsd/tokens to prevent double-counting
|
|
308
|
-
// (matches canonical rlm-query.ts:60-63).
|
|
309
|
-
emitter.emitSubcallUpdated({
|
|
310
|
-
id: subId,
|
|
311
|
-
status: "done",
|
|
312
|
-
resultPreview: res.answer.slice(0, 200),
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
return res.answer;
|
|
316
|
-
} catch (err) {
|
|
317
|
-
const msg = errorMessage(err);
|
|
318
|
-
emitter.emitSubcallUpdated({ id: subId, status: "error", detail: msg });
|
|
319
|
-
return formatError(`child RLM failed - ${msg}`);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
rlmQuery: rlmQueryImpl,
|
|
325
|
-
rlmQueryBatched: (prompts, model, depth) =>
|
|
326
|
-
mapPool(prompts, deps.config.maxConcurrentSubcalls, (p) => rlmQueryImpl(p, model, depth)),
|
|
327
|
-
};
|
|
121
|
+
/** Remaining budget/timeout of the invocation that currently owns the exec slot. */
|
|
122
|
+
remainingBudget(): RemainingResources | undefined {
|
|
123
|
+
const limits = this.currentLimits;
|
|
124
|
+
if (!limits) return undefined;
|
|
125
|
+
return { budgetUsd: limits.remainingBudgetUsd(), timeoutMs: limits.remainingTimeoutMs() };
|
|
328
126
|
}
|
|
329
127
|
}
|
|
330
128
|
|
|
@@ -337,8 +135,8 @@ export interface ReplToolDeps {
|
|
|
337
135
|
readonly getModel?: () => Model<Api> | undefined;
|
|
338
136
|
readonly getWorkerModel?: () => Model<Api> | undefined;
|
|
339
137
|
readonly registry: ModelRegistry;
|
|
340
|
-
|
|
341
|
-
readonly
|
|
138
|
+
/** Live accessor — `/rlm-config` replaces the config object, so never capture the value. */
|
|
139
|
+
readonly getConfig: () => RlmConfig;
|
|
342
140
|
readonly signal?: AbortSignal;
|
|
343
141
|
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
344
142
|
readonly ensureContext?: () => Promise<void>;
|
|
@@ -347,40 +145,64 @@ export interface ReplToolDeps {
|
|
|
347
145
|
}
|
|
348
146
|
|
|
349
147
|
export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplToolParams, ReplDetails> {
|
|
350
|
-
const { sandboxManager, workerModel, registry,
|
|
148
|
+
const { sandboxManager, workerModel, registry, getConfig, signal, onUsage } = deps;
|
|
351
149
|
const bridgeState = new NativeBridgeState();
|
|
352
150
|
|
|
353
151
|
// Late-bound cwd — getOrCreate installs handlers only at spawn; never rebuild the closure.
|
|
354
152
|
let sessionCwd = process.cwd();
|
|
355
153
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
154
|
+
const rootModel = (): Model<Api> => deps.getModel?.() ?? deps.model;
|
|
155
|
+
|
|
156
|
+
// Build handlers once — llm/rlm/library read late-bound state so the same closures stay
|
|
157
|
+
// correct across repl() calls; counters reset when the sandbox is discarded and re-spawned.
|
|
158
|
+
const llmHandlers = createLlmBridge({
|
|
159
|
+
workerModel: () => deps.getWorkerModel?.() ?? workerModel,
|
|
361
160
|
registry,
|
|
362
|
-
|
|
363
|
-
maxConcurrent: config.maxConcurrentSubcalls,
|
|
364
|
-
sampling: config.subSampling,
|
|
365
|
-
subSystem: config.subSystemPrompt,
|
|
161
|
+
config: getConfig,
|
|
366
162
|
signal,
|
|
163
|
+
onUsage: (usage) => { bridgeState.currentLimits?.addUsage(usage); },
|
|
164
|
+
remainingBudget: () => bridgeState.remainingBudget(),
|
|
165
|
+
emitter: () => bridgeState.currentEmitter ?? undefined,
|
|
166
|
+
parentId: () => bridgeState.currentParentId,
|
|
167
|
+
depth: () => bridgeState.currentDepth,
|
|
367
168
|
});
|
|
368
169
|
|
|
369
|
-
// Real recursive rlm_query
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
170
|
+
// Real recursive rlm_query — each call spawns a child RLM with its own sandbox and turn
|
|
171
|
+
// loop, bound to the *current* invocation's emitter so child sub-calls, turn progress, and
|
|
172
|
+
// cost deltas land on the live visual tree.
|
|
173
|
+
const runChildRlm: RunRlm = (input) => {
|
|
174
|
+
const emitter = bridgeState.currentEmitter;
|
|
175
|
+
// Only reachable while an invocation owns the exec slot, which always swaps in an emitter.
|
|
176
|
+
if (!emitter) throw new Error("RLM bridge not wired for this invocation");
|
|
177
|
+
const config = getConfig();
|
|
178
|
+
return createEngine({
|
|
179
|
+
model: rootModel(),
|
|
180
|
+
workerModel: deps.getWorkerModel?.() ?? workerModel,
|
|
181
|
+
registry,
|
|
182
|
+
config,
|
|
183
|
+
signal,
|
|
184
|
+
emitter,
|
|
185
|
+
onUsage: onUsage === undefined ? undefined : (usage, role) => { if (role === "sub") onUsage(usage, role); },
|
|
186
|
+
limits: limitsFromConfig(config),
|
|
187
|
+
onTodo: bridgeState.currentInteractive?.onTodo,
|
|
188
|
+
onAskUserQuestion: bridgeState.currentInteractive?.onAskUserQuestion,
|
|
189
|
+
})(input);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const rlmHandlers = createRlmHandlers({
|
|
193
|
+
run: runChildRlm,
|
|
194
|
+
llm: llmHandlers,
|
|
195
|
+
config: getConfig,
|
|
196
|
+
modelLabel: (override) => displayModelRef(registry, override, rootModel()),
|
|
197
|
+
emitter: () => bridgeState.currentEmitter ?? undefined,
|
|
198
|
+
parentNodeId: () => bridgeState.currentParentId,
|
|
199
|
+
remainingBudget: () => bridgeState.remainingBudget(),
|
|
200
|
+
onChildUsage: (costUsd, inputTokens, outputTokens) => {
|
|
201
|
+
bridgeState.currentLimits?.addRaw(costUsd, inputTokens, outputTokens);
|
|
202
|
+
},
|
|
381
203
|
});
|
|
382
204
|
|
|
383
|
-
const libraryBundle =
|
|
205
|
+
const libraryBundle = getConfig().libraryLoader
|
|
384
206
|
? buildLibraryHandler({
|
|
385
207
|
getCwd: () => sessionCwd,
|
|
386
208
|
getEmitter: () => bridgeState.currentEmitter,
|
|
@@ -396,11 +218,19 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
396
218
|
label: "REPL",
|
|
397
219
|
description:
|
|
398
220
|
"PRIMARY tool for ALL repository reading and analysis (read/grep are disabled in RLM mode). " +
|
|
399
|
-
"Persistent Python sandbox with every file pre-loaded in `context`.
|
|
400
|
-
"
|
|
401
|
-
"
|
|
402
|
-
"
|
|
403
|
-
"
|
|
221
|
+
"Persistent Python sandbox with every file pre-loaded in `context`. Locate first with the " +
|
|
222
|
+
"free primitives search(query) / grep_context(pattern) / outline(path), then delegate the " +
|
|
223
|
+
"semantic reading to map_files / llm_query / llm_query_batched / llm_query_chunked " +
|
|
224
|
+
"(rlm_query for iterative sub-tasks) — stdout returned to you is hard-capped at 4K chars, " +
|
|
225
|
+
"so printing file bodies is useless. Variables, imports, and the `answers`/`plan` memo " +
|
|
226
|
+
"persist across calls. Also supports todo, ask_user_question, and load_library.",
|
|
227
|
+
promptSnippet:
|
|
228
|
+
"repl: run Python in a persistent sandbox holding the whole repository in `context`; " +
|
|
229
|
+
"search/grep_context/outline to locate, map_files/llm_query* to read.",
|
|
230
|
+
promptGuidelines: [
|
|
231
|
+
"In RLM mode, read the repository through `repl` only — `read`/`grep` and bash readers are blocked.",
|
|
232
|
+
"Inside `repl`, locate with search()/grep_context()/outline() before delegating bulk reading to map_files()/llm_query_batched().",
|
|
233
|
+
],
|
|
404
234
|
parameters: ReplToolParams,
|
|
405
235
|
|
|
406
236
|
async execute(_toolCallId, rawParams, _execSignal, onUpdate, ctx) {
|
|
@@ -421,12 +251,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
421
251
|
let capturedStderr = "";
|
|
422
252
|
let progressStatus: ReplDetails["status"] = "running";
|
|
423
253
|
const startedAt = Date.now();
|
|
424
|
-
const limits = new LimitGuard(
|
|
425
|
-
maxBudgetUsd: config.maxBudgetUsd,
|
|
426
|
-
maxTimeoutMs: config.maxTimeoutMs,
|
|
427
|
-
maxTokens: config.maxTokens,
|
|
428
|
-
maxErrors: config.maxErrors,
|
|
429
|
-
});
|
|
254
|
+
const limits = new LimitGuard(limitsFromConfig(getConfig()));
|
|
430
255
|
|
|
431
256
|
// ── Progressive rendering: spinner + live sub-call tree ──
|
|
432
257
|
const progress = createProgressNotifier<ReplDetails>({
|
|
@@ -458,7 +283,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
458
283
|
// Build interactive handlers (session-stable callbacks)
|
|
459
284
|
const interactive = createPiInteractiveDeps(ctx);
|
|
460
285
|
const interactiveHandlers = buildInteractiveHandlers({
|
|
461
|
-
onAskUserQuestion:
|
|
286
|
+
onAskUserQuestion: getConfig().askUserQuestion ? interactive.onAskUserQuestion : undefined,
|
|
462
287
|
onTodo: interactive.onTodo,
|
|
463
288
|
onTodoRow: undefined,
|
|
464
289
|
emitter,
|
|
@@ -508,14 +333,11 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
508
333
|
if (queuedId) emitter.emitSubcallUpdated({ id: queuedId, status: "done" });
|
|
509
334
|
|
|
510
335
|
const finalAnswer = result.finalAnswer ?? undefined;
|
|
511
|
-
const { text: resultText
|
|
336
|
+
const { text: resultText } = buildReplResultText(
|
|
512
337
|
result.stdout,
|
|
513
338
|
finalAnswer,
|
|
514
|
-
result.edits,
|
|
515
|
-
result.raised,
|
|
516
339
|
store.getSubcalls(),
|
|
517
340
|
);
|
|
518
|
-
editRegistry?.registerAll(surfacedEdits);
|
|
519
341
|
|
|
520
342
|
const details: ReplDetails = {
|
|
521
343
|
status: "done",
|
|
@@ -525,7 +347,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
525
347
|
subcalls: store.getSubcalls(),
|
|
526
348
|
totals: store.getTotals(),
|
|
527
349
|
finalAnswer,
|
|
528
|
-
|
|
350
|
+
warnings: collectReplWarnings(store.getSubcalls()),
|
|
529
351
|
};
|
|
530
352
|
const progressText = finalAnswer !== undefined
|
|
531
353
|
? `ANSWER_SUBMITTED (${finalAnswer.length} chars)`
|
|
@@ -557,10 +379,8 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
557
379
|
},
|
|
558
380
|
|
|
559
381
|
renderCall(args, theme) {
|
|
560
|
-
const preview = args.code.length > 80 ? `${args.code.slice(0, 80)}...` : args.code;
|
|
561
382
|
return new Text(
|
|
562
|
-
theme.fg("toolTitle", theme.bold("repl ")) +
|
|
563
|
-
theme.fg("dim", preview.replace(/\n/g, " ")),
|
|
383
|
+
theme.fg("toolTitle", theme.bold("repl ")) + theme.fg("dim", previewText(args.code, CALL_PREVIEW_CHARS)),
|
|
564
384
|
0, 0,
|
|
565
385
|
);
|
|
566
386
|
},
|
|
@@ -579,29 +399,13 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
579
399
|
|
|
580
400
|
// ── Collapsed view ──
|
|
581
401
|
|
|
582
|
-
function
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const parts: string[] = [];
|
|
588
|
-
parts.push(formatCost(details.totals.costUsd));
|
|
589
|
-
if (details.totals.tokens > 0) parts.push(`${formatTokens(details.totals.tokens)} tok`);
|
|
590
|
-
if (details.executionTimeMs > 0) parts.push(`${details.executionTimeMs}ms`);
|
|
591
|
-
if (details.edits && details.edits.length > 0) {
|
|
592
|
-
parts.push(theme.fg("success", `${details.edits.length} staged`));
|
|
593
|
-
}
|
|
594
|
-
const stats = parts.length > 0 ? ` ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
595
|
-
|
|
596
|
-
const header = `${glyph} ${theme.fg("toolTitle", theme.bold("REPL"))}${stats}`;
|
|
597
|
-
|
|
598
|
-
let body = "";
|
|
599
|
-
if (details.subcalls.length > 0) {
|
|
600
|
-
body = `\n${renderCollapsedSubcallTree(details.subcalls, theme)}`;
|
|
601
|
-
}
|
|
402
|
+
function replStats(details: ReplDetails, theme: Theme): string {
|
|
403
|
+
const elapsed = details.executionTimeMs > 0 ? `${details.executionTimeMs}ms` : undefined;
|
|
404
|
+
return cardStatsLine(details.totals, theme, elapsed);
|
|
405
|
+
}
|
|
602
406
|
|
|
603
|
-
|
|
604
|
-
return
|
|
407
|
+
function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
408
|
+
return renderCollapsedCard("REPL", details.status, replStats(details, theme), details.subcalls, theme);
|
|
605
409
|
}
|
|
606
410
|
|
|
607
411
|
// ── Expanded view ──
|
|
@@ -609,35 +413,26 @@ function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
|
609
413
|
function renderReplExpanded(details: ReplDetails, theme: Theme): Container {
|
|
610
414
|
const container = new Container();
|
|
611
415
|
|
|
612
|
-
|
|
613
|
-
const glyph = details.status === "error" ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
614
|
-
const parts: string[] = [];
|
|
615
|
-
parts.push(formatCost(details.totals.costUsd));
|
|
616
|
-
if (details.totals.tokens > 0) parts.push(`${formatTokens(details.totals.tokens)} tok`);
|
|
617
|
-
if (details.executionTimeMs > 0) parts.push(`${details.executionTimeMs}ms`);
|
|
618
|
-
const stats = parts.length > 0 ? ` · ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
619
|
-
container.addChild(new Text(`${glyph} ${theme.fg("toolTitle", theme.bold("REPL"))}${stats}`, 0, 0));
|
|
416
|
+
container.addChild(new Text(cardHeader("REPL", details.status, replStats(details, theme), theme), 0, 0));
|
|
620
417
|
|
|
621
418
|
// Output
|
|
622
419
|
if (details.output) {
|
|
623
420
|
container.addChild(new Spacer(1));
|
|
624
|
-
const out = details.output.length >
|
|
421
|
+
const out = details.output.length > EXPANDED_STDOUT_CHARS
|
|
422
|
+
? `${details.output.slice(0, EXPANDED_STDOUT_CHARS)}…`
|
|
423
|
+
: details.output;
|
|
625
424
|
container.addChild(new Text(out, 0, 0));
|
|
626
425
|
}
|
|
627
426
|
|
|
628
|
-
if (details.
|
|
629
|
-
const editFiles = new Set<string>();
|
|
630
|
-
for (const edit of details.edits) editFiles.add(edit.path);
|
|
427
|
+
if (details.warnings && details.warnings.length > 0) {
|
|
631
428
|
container.addChild(new Spacer(1));
|
|
632
|
-
container.addChild(new Text(theme.fg("
|
|
633
|
-
`${details.edits.length} edit${details.edits.length > 1 ? "s" : ""} staged across ${editFiles.size} file${editFiles.size > 1 ? "s" : ""}`,
|
|
634
|
-
), 0, 0));
|
|
429
|
+
container.addChild(new Text(theme.fg("muted", details.warnings.join("\n")), 0, 0));
|
|
635
430
|
}
|
|
636
431
|
|
|
637
432
|
// Stderr
|
|
638
433
|
if (details.stderr) {
|
|
639
434
|
container.addChild(new Spacer(1));
|
|
640
|
-
container.addChild(new Text(theme.fg("error", details.stderr.slice(0,
|
|
435
|
+
container.addChild(new Text(theme.fg("error", details.stderr.slice(0, EXPANDED_STDERR_CHARS)), 0, 0));
|
|
641
436
|
}
|
|
642
437
|
|
|
643
438
|
// Sub-call tree
|