@hicaru/pi-rlm 0.1.9 → 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/package.json +1 -1
- package/src/bridge/llm-query.ts +59 -36
- package/src/bridge/rlm-query.ts +63 -77
- 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/repomix-context.ts +5 -10
- package/src/core/answer.ts +4 -3
- package/src/core/artifacts.ts +4 -3
- package/src/core/engine.ts +48 -249
- package/src/core/gates.ts +3 -3
- package/src/core/limits.ts +19 -1
- package/src/core/pipeline-handlers.ts +319 -0
- package/src/core/pipeline.ts +2 -2
- package/src/core/types.ts +25 -27
- package/src/index.ts +35 -17
- package/src/mode/rlm-mode.ts +8 -11
- package/src/prompts/system.ts +143 -52
- package/src/prompts/user.ts +1 -5
- package/src/sandbox/protocol.ts +0 -7
- package/src/sandbox/sandbox-manager.ts +5 -5
- package/src/sandbox/sandbox.ts +25 -19
- package/src/sandbox/worker.py +354 -0
- package/src/state/paths.ts +1 -1
- package/src/state/reads.ts +12 -4
- package/src/state/resume.ts +5 -11
- package/src/text/parsing.ts +0 -6
- package/src/tool/repl-tool.ts +105 -284
- package/src/tool/rlm-details.ts +0 -10
- package/src/tool/rlm-tool.ts +18 -31
- package/src/tool/subcall-render.ts +61 -9
- package/src/tool/subcall-store.ts +2 -2
- package/src/ui/config-panel.ts +41 -21
- 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/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,34 +17,39 @@ 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 { SubLlmHandlers } from "../sandbox/sandbox.ts";
|
|
31
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 {
|
|
36
|
+
import { spinnerFrame } from "../ui/theme.ts";
|
|
37
|
+
import { previewText } from "../text/preview.ts";
|
|
38
|
+
import { errorMessage } from "../util/errors.ts";
|
|
39
39
|
import {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
cardHeader,
|
|
41
|
+
cardStatsLine,
|
|
42
|
+
renderCollapsedCard,
|
|
42
43
|
renderExpandedSubcallTree,
|
|
43
44
|
} from "./subcall-render.ts";
|
|
44
45
|
import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
|
|
45
46
|
import { capReplResultText, replDelegationNudge } from "../mode/native-guards.ts";
|
|
46
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
|
+
|
|
47
53
|
// ── Parameter schema ──
|
|
48
54
|
|
|
49
55
|
export const ReplToolParams = Object.freeze(Type.Object({
|
|
@@ -93,10 +99,9 @@ export function collectReplWarnings(subcalls: readonly RlmSubcall[]): readonly s
|
|
|
93
99
|
// ── Mutable bridge state (handler indirection) ──
|
|
94
100
|
|
|
95
101
|
/**
|
|
96
|
-
* Holds per-invocation mutable state that
|
|
97
|
-
* The sandbox is created once with handlers
|
|
98
|
-
*
|
|
99
|
-
* 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).
|
|
100
105
|
*/
|
|
101
106
|
class NativeBridgeState {
|
|
102
107
|
currentEmitter: RlmEmitter | null = null;
|
|
@@ -113,203 +118,11 @@ class NativeBridgeState {
|
|
|
113
118
|
this.currentInteractive = inv.interactive;
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
maxConcurrent: number;
|
|
122
|
-
sampling?: Sampling;
|
|
123
|
-
subSystem?: string;
|
|
124
|
-
signal?: AbortSignal;
|
|
125
|
-
}): Pick<SubLlmHandlers, "llmQuery" | "llmQueryBatched"> {
|
|
126
|
-
const state = this;
|
|
127
|
-
|
|
128
|
-
const workerModel = (): Model<Api> => deps.getWorkerModel?.() ?? deps.workerModel;
|
|
129
|
-
const displayModel = (model: string | null): string =>
|
|
130
|
-
modelRef(model ? (resolveModelId(deps.registry, model) ?? workerModel()) : workerModel()) ?? workerModel().id;
|
|
131
|
-
|
|
132
|
-
async function complete1(prompt: string, model: string | null, track: (u: Usage) => void): Promise<string> {
|
|
133
|
-
const limits = state.currentLimits;
|
|
134
|
-
if (limits) {
|
|
135
|
-
const limitError = checkResourceLimits({ budgetUsd: limits.remainingBudgetUsd(), timeoutMs: limits.remainingTimeoutMs() });
|
|
136
|
-
if (limitError !== undefined) return limitError;
|
|
137
|
-
}
|
|
138
|
-
if (prompt.length > deps.maxPromptChars) {
|
|
139
|
-
return formatError(`sub-LLM prompt exceeded size limit (${prompt.length.toLocaleString()} chars > ${deps.maxPromptChars.toLocaleString()})`);
|
|
140
|
-
}
|
|
141
|
-
const resolved = model ? resolveModelId(deps.registry, model) : undefined;
|
|
142
|
-
if (model && !resolved) return formatError(`unknown model override '${model}'`);
|
|
143
|
-
try {
|
|
144
|
-
const messages: ChatMsg[] = [{ role: "user", content: prompt }];
|
|
145
|
-
const res = await modelComplete(messages, {
|
|
146
|
-
model: resolved ?? workerModel(),
|
|
147
|
-
registry: deps.registry,
|
|
148
|
-
system: deps.subSystem,
|
|
149
|
-
maxTokens: deps.sampling?.maxTokens,
|
|
150
|
-
temperature: deps.sampling?.temperature,
|
|
151
|
-
reasoning: deps.sampling?.reasoning,
|
|
152
|
-
signal: deps.signal,
|
|
153
|
-
});
|
|
154
|
-
limits?.addUsage(res.usage);
|
|
155
|
-
track(res.usage);
|
|
156
|
-
return res.text;
|
|
157
|
-
} catch (err) {
|
|
158
|
-
const msg = errorMessage(err);
|
|
159
|
-
const hint = /credit|402|payment|quota|rate.limit/i.test(msg)
|
|
160
|
-
? " — try smaller batches or individual llm_query calls"
|
|
161
|
-
: "";
|
|
162
|
-
return formatError(`${msg}${hint}`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
async llmQuery(prompt, model, _depth) {
|
|
168
|
-
const id = state.currentEmitter?.emitSubcallCreated({
|
|
169
|
-
kind: "llm", parentId: state.currentParentId, label: "llm_query",
|
|
170
|
-
model: displayModel(model), args: `prompt: ${previewText(prompt)}`,
|
|
171
|
-
depth: state.currentDepth,
|
|
172
|
-
});
|
|
173
|
-
let cost = 0; let tokens = 0;
|
|
174
|
-
const out = await complete1(prompt, model, (u) => { cost += u.cost.total; tokens += u.totalTokens; });
|
|
175
|
-
if (id) state.currentEmitter?.emitSubcallUpdated({ id,
|
|
176
|
-
status: isErrorText(out) ? "error" : "done",
|
|
177
|
-
costUsd: cost, tokens, resultPreview: previewText(out),
|
|
178
|
-
detail: isErrorText(out) ? out : undefined,
|
|
179
|
-
});
|
|
180
|
-
return out;
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
async llmQueryBatched(prompts: readonly string[], model, _depth): Promise<string[]> {
|
|
184
|
-
const id = state.currentEmitter?.emitSubcallCreated({
|
|
185
|
-
kind: "batch", parentId: state.currentParentId, label: `llm_query ×${prompts.length}`,
|
|
186
|
-
model: displayModel(model), args: `prompt: ${previewText(prompts[0] ?? "")}`,
|
|
187
|
-
depth: state.currentDepth,
|
|
188
|
-
});
|
|
189
|
-
let cost = 0; let tokens = 0;
|
|
190
|
-
const out: string[] = await mapPool(prompts, deps.maxConcurrent, (p) =>
|
|
191
|
-
complete1(p, model, (u) => { cost += u.cost.total; tokens += u.totalTokens; }),
|
|
192
|
-
);
|
|
193
|
-
const failed = out.filter(isErrorText).length;
|
|
194
|
-
const allFailed = failed === out.length;
|
|
195
|
-
const error = allFailed
|
|
196
|
-
? `all ${out.length} sub-calls failed — reduce batch size or try llm_query individually`
|
|
197
|
-
: failed > 0 ? `${failed}/${out.length} sub-calls failed` : undefined;
|
|
198
|
-
if (id) state.currentEmitter?.emitSubcallUpdated({ id,
|
|
199
|
-
status: error ? "error" : "done", costUsd: cost, tokens,
|
|
200
|
-
resultPreview: previewText(out[0] ?? ""), detail: error,
|
|
201
|
-
failedCount: failed, totalCount: out.length,
|
|
202
|
-
});
|
|
203
|
-
return out;
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Build real recursive rlm_query / rlm_query_batched handlers that spawn
|
|
210
|
-
* child RLM engines (each with its own sandbox and turn loop) rather than
|
|
211
|
-
* falling back to a one-shot llm_query.
|
|
212
|
-
*
|
|
213
|
-
* At the maxDepth cap the handler degrades to a plain llm_query via the
|
|
214
|
-
* already-wired llmHandlers (which read from the same mutable state).
|
|
215
|
-
*/
|
|
216
|
-
buildRlmHandlers(deps: {
|
|
217
|
-
model: Model<Api>;
|
|
218
|
-
workerModel: Model<Api>;
|
|
219
|
-
getModel?: () => Model<Api> | undefined;
|
|
220
|
-
getWorkerModel?: () => Model<Api> | undefined;
|
|
221
|
-
registry: ModelRegistry;
|
|
222
|
-
config: RlmConfig;
|
|
223
|
-
signal?: AbortSignal;
|
|
224
|
-
onUsage?: (usage: Usage, role: "sub") => void;
|
|
225
|
-
llmHandlers: Pick<SubLlmHandlers, "llmQuery" | "llmQueryBatched">;
|
|
226
|
-
}): Pick<SubLlmHandlers, "rlmQuery" | "rlmQueryBatched"> {
|
|
227
|
-
const state = this;
|
|
228
|
-
|
|
229
|
-
async function rlmQueryImpl(prompt: string, model: string | null, depth: number): Promise<string> {
|
|
230
|
-
const emitter = state.currentEmitter;
|
|
231
|
-
const limits = state.currentLimits;
|
|
232
|
-
if (!emitter || !limits) return formatError("RLM bridge not wired for this invocation");
|
|
233
|
-
|
|
234
|
-
const childDepth = state.currentDepth + 1;
|
|
235
|
-
|
|
236
|
-
// Depth cap: degrade to a one-shot llm_query.
|
|
237
|
-
if (childDepth >= deps.config.maxDepth) {
|
|
238
|
-
return deps.llmHandlers.llmQuery(prompt, model, depth);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const remBudget = limits.remainingBudgetUsd();
|
|
242
|
-
const remTimeout = limits.remainingTimeoutMs();
|
|
243
|
-
const limitError = checkResourceLimits({ budgetUsd: remBudget, timeoutMs: remTimeout });
|
|
244
|
-
if (limitError) return limitError;
|
|
245
|
-
|
|
246
|
-
const rootModel = deps.getModel?.() ?? deps.model;
|
|
247
|
-
const workerModel = deps.getWorkerModel?.() ?? deps.workerModel;
|
|
248
|
-
const resolvedOverride = model ? resolveModelId(deps.registry, model) : undefined;
|
|
249
|
-
const subId = emitter.emitSubcallCreated({
|
|
250
|
-
kind: "rlm", parentId: state.currentParentId, label: "rlm_query",
|
|
251
|
-
model: model ? (modelRef(resolvedOverride) ?? `unknown/${model}`) : (modelRef(rootModel) ?? rootModel.id),
|
|
252
|
-
detail: prompt.slice(0, 60),
|
|
253
|
-
depth: childDepth,
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// Per-call engine creation with the visible emitter — child llm_query subcalls,
|
|
257
|
-
// turn progress, and cost deltas land on the per-invocation emitter, visible to
|
|
258
|
-
// SubcallStore and the live visual tree.
|
|
259
|
-
const runRlm = createEngine({
|
|
260
|
-
model: rootModel,
|
|
261
|
-
workerModel,
|
|
262
|
-
registry: deps.registry,
|
|
263
|
-
config: deps.config,
|
|
264
|
-
signal: deps.signal,
|
|
265
|
-
emitter: emitter,
|
|
266
|
-
onUsage: deps.onUsage as ((usage: Usage, role: "root" | "sub") => void) | undefined,
|
|
267
|
-
limits: {
|
|
268
|
-
maxBudgetUsd: deps.config.maxBudgetUsd,
|
|
269
|
-
maxTimeoutMs: deps.config.maxTimeoutMs,
|
|
270
|
-
maxTokens: deps.config.maxTokens,
|
|
271
|
-
maxErrors: deps.config.maxErrors,
|
|
272
|
-
},
|
|
273
|
-
onTodo: state.currentInteractive?.onTodo,
|
|
274
|
-
onAskUserQuestion: state.currentInteractive?.onAskUserQuestion,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const res = await runRlm({
|
|
279
|
-
rootPrompt: "",
|
|
280
|
-
context: prompt,
|
|
281
|
-
depth: childDepth,
|
|
282
|
-
parentNodeId: subId,
|
|
283
|
-
modelOverride: model ?? undefined,
|
|
284
|
-
remainingBudgetUsd: remBudget,
|
|
285
|
-
remainingTimeoutMs: remTimeout,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// Debit parent limit guard for the entire child run.
|
|
289
|
-
limits.addRaw(res.costUsd, res.inputTokens, res.outputTokens);
|
|
290
|
-
|
|
291
|
-
// Child engine emits live usage deltas via the shared emitter — SubcallStore
|
|
292
|
-
// accumulates them. No final aggregate costUsd/tokens to prevent double-counting
|
|
293
|
-
// (matches canonical rlm-query.ts:60-63).
|
|
294
|
-
emitter.emitSubcallUpdated({
|
|
295
|
-
id: subId,
|
|
296
|
-
status: "done",
|
|
297
|
-
resultPreview: res.answer.slice(0, 200),
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
return res.answer;
|
|
301
|
-
} catch (err) {
|
|
302
|
-
const msg = errorMessage(err);
|
|
303
|
-
emitter.emitSubcallUpdated({ id: subId, status: "error", detail: msg });
|
|
304
|
-
return formatError(`child RLM failed - ${msg}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
rlmQuery: rlmQueryImpl,
|
|
310
|
-
rlmQueryBatched: (prompts, model, depth) =>
|
|
311
|
-
mapPool(prompts, deps.config.maxConcurrentSubcalls, (p) => rlmQueryImpl(p, model, depth)),
|
|
312
|
-
};
|
|
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() };
|
|
313
126
|
}
|
|
314
127
|
}
|
|
315
128
|
|
|
@@ -322,7 +135,8 @@ export interface ReplToolDeps {
|
|
|
322
135
|
readonly getModel?: () => Model<Api> | undefined;
|
|
323
136
|
readonly getWorkerModel?: () => Model<Api> | undefined;
|
|
324
137
|
readonly registry: ModelRegistry;
|
|
325
|
-
|
|
138
|
+
/** Live accessor — `/rlm-config` replaces the config object, so never capture the value. */
|
|
139
|
+
readonly getConfig: () => RlmConfig;
|
|
326
140
|
readonly signal?: AbortSignal;
|
|
327
141
|
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
328
142
|
readonly ensureContext?: () => Promise<void>;
|
|
@@ -331,40 +145,64 @@ export interface ReplToolDeps {
|
|
|
331
145
|
}
|
|
332
146
|
|
|
333
147
|
export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplToolParams, ReplDetails> {
|
|
334
|
-
const { sandboxManager, workerModel, registry,
|
|
148
|
+
const { sandboxManager, workerModel, registry, getConfig, signal, onUsage } = deps;
|
|
335
149
|
const bridgeState = new NativeBridgeState();
|
|
336
150
|
|
|
337
151
|
// Late-bound cwd — getOrCreate installs handlers only at spawn; never rebuild the closure.
|
|
338
152
|
let sessionCwd = process.cwd();
|
|
339
153
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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,
|
|
345
160
|
registry,
|
|
346
|
-
|
|
347
|
-
maxConcurrent: config.maxConcurrentSubcalls,
|
|
348
|
-
sampling: config.subSampling,
|
|
349
|
-
subSystem: config.subSystemPrompt,
|
|
161
|
+
config: getConfig,
|
|
350
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,
|
|
351
168
|
});
|
|
352
169
|
|
|
353
|
-
// Real recursive rlm_query
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
},
|
|
365
203
|
});
|
|
366
204
|
|
|
367
|
-
const libraryBundle =
|
|
205
|
+
const libraryBundle = getConfig().libraryLoader
|
|
368
206
|
? buildLibraryHandler({
|
|
369
207
|
getCwd: () => sessionCwd,
|
|
370
208
|
getEmitter: () => bridgeState.currentEmitter,
|
|
@@ -380,11 +218,19 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
380
218
|
label: "REPL",
|
|
381
219
|
description:
|
|
382
220
|
"PRIMARY tool for ALL repository reading and analysis (read/grep are disabled in RLM mode). " +
|
|
383
|
-
"Persistent Python sandbox with every file pre-loaded in `context`.
|
|
384
|
-
"
|
|
385
|
-
"
|
|
386
|
-
"
|
|
387
|
-
"
|
|
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
|
+
],
|
|
388
234
|
parameters: ReplToolParams,
|
|
389
235
|
|
|
390
236
|
async execute(_toolCallId, rawParams, _execSignal, onUpdate, ctx) {
|
|
@@ -405,12 +251,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
405
251
|
let capturedStderr = "";
|
|
406
252
|
let progressStatus: ReplDetails["status"] = "running";
|
|
407
253
|
const startedAt = Date.now();
|
|
408
|
-
const limits = new LimitGuard(
|
|
409
|
-
maxBudgetUsd: config.maxBudgetUsd,
|
|
410
|
-
maxTimeoutMs: config.maxTimeoutMs,
|
|
411
|
-
maxTokens: config.maxTokens,
|
|
412
|
-
maxErrors: config.maxErrors,
|
|
413
|
-
});
|
|
254
|
+
const limits = new LimitGuard(limitsFromConfig(getConfig()));
|
|
414
255
|
|
|
415
256
|
// ── Progressive rendering: spinner + live sub-call tree ──
|
|
416
257
|
const progress = createProgressNotifier<ReplDetails>({
|
|
@@ -442,7 +283,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
442
283
|
// Build interactive handlers (session-stable callbacks)
|
|
443
284
|
const interactive = createPiInteractiveDeps(ctx);
|
|
444
285
|
const interactiveHandlers = buildInteractiveHandlers({
|
|
445
|
-
onAskUserQuestion:
|
|
286
|
+
onAskUserQuestion: getConfig().askUserQuestion ? interactive.onAskUserQuestion : undefined,
|
|
446
287
|
onTodo: interactive.onTodo,
|
|
447
288
|
onTodoRow: undefined,
|
|
448
289
|
emitter,
|
|
@@ -538,10 +379,8 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
538
379
|
},
|
|
539
380
|
|
|
540
381
|
renderCall(args, theme) {
|
|
541
|
-
const preview = args.code.length > 80 ? `${args.code.slice(0, 80)}...` : args.code;
|
|
542
382
|
return new Text(
|
|
543
|
-
theme.fg("toolTitle", theme.bold("repl ")) +
|
|
544
|
-
theme.fg("dim", preview.replace(/\n/g, " ")),
|
|
383
|
+
theme.fg("toolTitle", theme.bold("repl ")) + theme.fg("dim", previewText(args.code, CALL_PREVIEW_CHARS)),
|
|
545
384
|
0, 0,
|
|
546
385
|
);
|
|
547
386
|
},
|
|
@@ -560,26 +399,13 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
560
399
|
|
|
561
400
|
// ── Collapsed view ──
|
|
562
401
|
|
|
563
|
-
function
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const parts: string[] = [];
|
|
569
|
-
parts.push(formatCost(details.totals.costUsd));
|
|
570
|
-
if (details.totals.tokens > 0) parts.push(`${formatTokens(details.totals.tokens)} tok`);
|
|
571
|
-
if (details.executionTimeMs > 0) parts.push(`${details.executionTimeMs}ms`);
|
|
572
|
-
const stats = parts.length > 0 ? ` ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
573
|
-
|
|
574
|
-
const header = `${glyph} ${theme.fg("toolTitle", theme.bold("REPL"))}${stats}`;
|
|
575
|
-
|
|
576
|
-
let body = "";
|
|
577
|
-
if (details.subcalls.length > 0) {
|
|
578
|
-
body = `\n${renderCollapsedSubcallTree(details.subcalls, theme)}`;
|
|
579
|
-
}
|
|
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
|
+
}
|
|
580
406
|
|
|
581
|
-
|
|
582
|
-
return
|
|
407
|
+
function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
408
|
+
return renderCollapsedCard("REPL", details.status, replStats(details, theme), details.subcalls, theme);
|
|
583
409
|
}
|
|
584
410
|
|
|
585
411
|
// ── Expanded view ──
|
|
@@ -587,19 +413,14 @@ function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
|
587
413
|
function renderReplExpanded(details: ReplDetails, theme: Theme): Container {
|
|
588
414
|
const container = new Container();
|
|
589
415
|
|
|
590
|
-
|
|
591
|
-
const glyph = details.status === "error" ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
592
|
-
const parts: string[] = [];
|
|
593
|
-
parts.push(formatCost(details.totals.costUsd));
|
|
594
|
-
if (details.totals.tokens > 0) parts.push(`${formatTokens(details.totals.tokens)} tok`);
|
|
595
|
-
if (details.executionTimeMs > 0) parts.push(`${details.executionTimeMs}ms`);
|
|
596
|
-
const stats = parts.length > 0 ? ` · ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
597
|
-
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));
|
|
598
417
|
|
|
599
418
|
// Output
|
|
600
419
|
if (details.output) {
|
|
601
420
|
container.addChild(new Spacer(1));
|
|
602
|
-
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;
|
|
603
424
|
container.addChild(new Text(out, 0, 0));
|
|
604
425
|
}
|
|
605
426
|
|
|
@@ -611,7 +432,7 @@ function renderReplExpanded(details: ReplDetails, theme: Theme): Container {
|
|
|
611
432
|
// Stderr
|
|
612
433
|
if (details.stderr) {
|
|
613
434
|
container.addChild(new Spacer(1));
|
|
614
|
-
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));
|
|
615
436
|
}
|
|
616
437
|
|
|
617
438
|
// Sub-call tree
|
package/src/tool/rlm-details.ts
CHANGED
|
@@ -44,13 +44,3 @@ export interface RlmDetails {
|
|
|
44
44
|
readonly warnings?: readonly string[];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export interface SubcallInit {
|
|
48
|
-
readonly parentId?: string;
|
|
49
|
-
readonly kind: SubcallKind;
|
|
50
|
-
readonly label: string;
|
|
51
|
-
readonly model?: string;
|
|
52
|
-
readonly detail?: string;
|
|
53
|
-
readonly args?: string;
|
|
54
|
-
/** Recursion depth. Required — all call sites pass this. */
|
|
55
|
-
readonly depth: number;
|
|
56
|
-
}
|
package/src/tool/rlm-tool.ts
CHANGED
|
@@ -5,23 +5,29 @@
|
|
|
5
5
|
* onUpdate(partialResult) for progressive TUI re-rendering.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { type Theme, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Container, Markdown, Spacer, Text, type Component } from "@earendil-works/pi-tui";
|
|
10
10
|
import { Type } from "typebox";
|
|
11
11
|
import { createPiInteractiveDeps } from "../bridge/pi-interactive.ts";
|
|
12
12
|
import type { RlmController, StartInput } from "../mode/rlm-mode.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { spinnerFrame } from "../ui/theme.ts";
|
|
14
|
+
import { markdownTheme } from "../ui/theme-adapter.ts";
|
|
15
|
+
import { previewText } from "../text/preview.ts";
|
|
14
16
|
import { errorMessage } from "../util/errors.ts";
|
|
15
17
|
import { type RlmDetails } from "./rlm-details.ts";
|
|
16
18
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
17
19
|
import { RlmEventAggregator } from "./rlm-aggregator.ts";
|
|
18
20
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
cardHeader,
|
|
22
|
+
cardStatsLine,
|
|
23
|
+
renderCollapsedCard,
|
|
21
24
|
renderExpandedSubcallTree,
|
|
22
25
|
} from "./subcall-render.ts";
|
|
23
26
|
import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
|
|
24
27
|
|
|
28
|
+
/** Chars of the prompt shown on the tool call line. */
|
|
29
|
+
const CALL_PREVIEW_CHARS = 80;
|
|
30
|
+
|
|
25
31
|
// ── Parameter schema ──
|
|
26
32
|
|
|
27
33
|
export const RlmToolParams = Object.freeze(Type.Object({
|
|
@@ -32,11 +38,8 @@ export const RlmToolParams = Object.freeze(Type.Object({
|
|
|
32
38
|
// ── Rendering helpers ──
|
|
33
39
|
|
|
34
40
|
function rootStats(details: RlmDetails, theme: Theme): string {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
parts.push(`${formatTokens(details.totals.tokens)} tok`);
|
|
38
|
-
if (details.turns.current > 0) parts.push(`${details.turns.current} turn${details.turns.current > 1 ? "s" : ""}`);
|
|
39
|
-
return theme.fg("dim", parts.join(" · "));
|
|
41
|
+
const turns = details.turns.current;
|
|
42
|
+
return cardStatsLine(details.totals, theme, turns > 0 ? `${turns} turn${turns > 1 ? "s" : ""}` : undefined);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
// ── Tool definition ──
|
|
@@ -110,18 +113,14 @@ export function createRlmTool(controller: RlmController): ToolDefinition<typeof
|
|
|
110
113
|
}
|
|
111
114
|
},
|
|
112
115
|
|
|
113
|
-
renderCall(args, theme
|
|
114
|
-
const preview = args.prompt.length > 80
|
|
115
|
-
? `${args.prompt.slice(0, 80)}...`
|
|
116
|
-
: args.prompt;
|
|
116
|
+
renderCall(args, theme) {
|
|
117
117
|
return new Text(
|
|
118
|
-
theme.fg("toolTitle", theme.bold("rlm ")) +
|
|
119
|
-
theme.fg("dim", preview.replace(/\n/g, " ")),
|
|
118
|
+
theme.fg("toolTitle", theme.bold("rlm ")) + theme.fg("dim", previewText(args.prompt, CALL_PREVIEW_CHARS)),
|
|
120
119
|
0, 0,
|
|
121
120
|
);
|
|
122
121
|
},
|
|
123
122
|
|
|
124
|
-
renderResult(result, { expanded
|
|
123
|
+
renderResult(result, { expanded }, theme) {
|
|
125
124
|
const details = result.details as RlmDetails | undefined;
|
|
126
125
|
if (!details) {
|
|
127
126
|
const text = result.content[0];
|
|
@@ -139,10 +138,7 @@ export function createRlmTool(controller: RlmController): ToolDefinition<typeof
|
|
|
139
138
|
|
|
140
139
|
function renderExpanded(details: RlmDetails, theme: Theme): Component {
|
|
141
140
|
const container = new Container();
|
|
142
|
-
|
|
143
|
-
const glyph = headlineStatusGlyph(details.status, theme);
|
|
144
|
-
const header = `${glyph} ${theme.fg("toolTitle", theme.bold("RLM"))} · ${rootStats(details, theme)}`;
|
|
145
|
-
container.addChild(new Text(header, 0, 0));
|
|
141
|
+
container.addChild(new Text(cardHeader("RLM", details.status, rootStats(details, theme), theme), 0, 0));
|
|
146
142
|
|
|
147
143
|
if (details.subcalls.length > 0) {
|
|
148
144
|
container.addChild(new Spacer(1));
|
|
@@ -153,7 +149,7 @@ function renderExpanded(details: RlmDetails, theme: Theme): Component {
|
|
|
153
149
|
if (details.answer) {
|
|
154
150
|
container.addChild(new Spacer(1));
|
|
155
151
|
container.addChild(new Text(theme.fg("muted", "─── Answer ───"), 0, 0));
|
|
156
|
-
container.addChild(new Markdown(details.answer, 0, 0,
|
|
152
|
+
container.addChild(new Markdown(details.answer, 0, 0, markdownTheme(theme)));
|
|
157
153
|
}
|
|
158
154
|
|
|
159
155
|
if (details.warnings && details.warnings.length > 0) {
|
|
@@ -167,14 +163,5 @@ function renderExpanded(details: RlmDetails, theme: Theme): Component {
|
|
|
167
163
|
// ── Collapsed view ──
|
|
168
164
|
|
|
169
165
|
function renderCollapsed(details: RlmDetails, theme: Theme): Text {
|
|
170
|
-
|
|
171
|
-
const header = `${glyph} ${theme.fg("toolTitle", theme.bold("RLM"))} · ${rootStats(details, theme)}`;
|
|
172
|
-
|
|
173
|
-
let body = "";
|
|
174
|
-
if (details.subcalls.length > 0) {
|
|
175
|
-
body = `\n${renderCollapsedSubcallTree(details.subcalls, theme)}`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const expandHint = details.status === "running" ? "" : `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
179
|
-
return new Text(`${header}${body}${expandHint}`, 0, 0);
|
|
166
|
+
return renderCollapsedCard("RLM", details.status, rootStats(details, theme), details.subcalls, theme);
|
|
180
167
|
}
|