@hicaru/pi-rlm 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -4
- package/package.json +2 -1
- package/src/bridge/library.ts +155 -0
- package/src/bridge/llm-query.ts +1 -0
- package/src/bridge/rlm-query.ts +56 -12
- package/src/config/defaults.ts +2 -0
- package/src/config/settings.ts +4 -0
- package/src/context/library-context.ts +266 -0
- package/src/context/repomix-context.ts +2 -48
- package/src/core/answer.ts +1 -10
- package/src/core/artifacts.ts +88 -0
- package/src/core/critique.ts +92 -0
- package/src/core/engine.ts +446 -53
- package/src/core/gates.ts +301 -0
- package/src/core/iteration.ts +7 -2
- package/src/core/pipeline.ts +196 -28
- package/src/core/types.ts +5 -3
- package/src/index.ts +3 -6
- package/src/mode/native-guards.ts +2 -2
- package/src/prompts/phases.ts +104 -0
- package/src/prompts/system.ts +59 -16
- package/src/prompts/user.ts +12 -4
- package/src/sandbox/protocol.ts +29 -11
- package/src/sandbox/sandbox.ts +77 -2
- package/src/sandbox/worker.py +215 -46
- package/src/state/index.ts +2 -1
- package/src/state/paths.ts +4 -2
- package/src/state/reads.ts +31 -2
- package/src/state/resume.ts +31 -6
- package/src/state/rows.ts +8 -2
- package/src/state/writes.ts +5 -3
- package/src/text/tokens.ts +7 -1
- package/src/tool/repl-details.ts +2 -3
- package/src/tool/repl-tool.ts +52 -57
- package/src/tool/rlm-aggregator.ts +7 -7
- package/src/tool/rlm-details.ts +6 -3
- package/src/tool/rlm-events.ts +14 -11
- package/src/tool/rlm-tool.ts +2 -8
- package/src/tool/subcall-store.ts +2 -0
- package/src/ui/config-panel.ts +8 -1
- package/src/registry/edit-registry.ts +0 -22
- package/src/text/edits.ts +0 -16
- package/src/tool/apply-edits-tool.ts +0 -288
package/src/tool/repl-tool.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { Model, Usage, Api } from "@earendil-works/pi-ai";
|
|
|
18
18
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { modelRef, resolveModelId } from "../config/settings.ts";
|
|
20
20
|
import { buildInteractiveHandlers } from "../bridge/interactive.ts";
|
|
21
|
+
import { buildLibraryHandler } from "../bridge/library.ts";
|
|
21
22
|
import { createPiInteractiveDeps } from "../bridge/pi-interactive.ts";
|
|
22
23
|
import { type ChatMsg, modelComplete } from "../bridge/model.ts";
|
|
23
24
|
import { previewText } from "../text/preview.ts";
|
|
@@ -27,14 +28,13 @@ import { checkResourceLimits } from "../core/resource-limits.ts";
|
|
|
27
28
|
import type { InteractiveDeps, RlmConfig, Sampling } from "../core/types.ts";
|
|
28
29
|
import { SandboxManager } from "../sandbox/sandbox-manager.ts";
|
|
29
30
|
import type { SubLlmHandlers } from "../sandbox/sandbox.ts";
|
|
30
|
-
import type {
|
|
31
|
+
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
31
32
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
32
33
|
import { SubcallStore } from "./subcall-store.ts";
|
|
33
34
|
import type { ReplDetails } from "./repl-details.ts";
|
|
34
35
|
import type { RlmSubcall } from "./rlm-details.ts";
|
|
35
36
|
import { createEngine } from "../core/engine.ts";
|
|
36
37
|
import { formatCost, formatTokens, spinnerFrame } from "../ui/theme.ts";
|
|
37
|
-
import type { EditRegistry } from "../registry/edit-registry.ts";
|
|
38
38
|
import { errorMessage, formatError, isErrorText } from "../util/errors.ts";
|
|
39
39
|
import {
|
|
40
40
|
headlineStatusGlyph,
|
|
@@ -50,59 +50,44 @@ export const ReplToolParams = Object.freeze(Type.Object({
|
|
|
50
50
|
code: Type.String({ description: "Python code to execute in the persistent REPL sandbox" }),
|
|
51
51
|
}));
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
return edits.length > 0 && !raised ? edits : undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Model-visible text assembled from a repl() result, plus the surfaced edits for `details`. */
|
|
53
|
+
/** Model-visible text assembled from a repl() result. */
|
|
58
54
|
export interface ReplResultText {
|
|
59
55
|
readonly text: string;
|
|
60
|
-
readonly surfacedEdits: readonly ProposedEdit[] | undefined;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function countLines(text: string): number {
|
|
64
|
-
if (text.length === 0) return 0;
|
|
65
|
-
let count = 1;
|
|
66
|
-
for (const ch of text) if (ch === "\n") count++;
|
|
67
|
-
return count;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function stagedEditSummary(edits: readonly ProposedEdit[]): string {
|
|
71
|
-
const rows = new Array<string>(edits.length);
|
|
72
|
-
for (let i = 0; i < edits.length; i++) {
|
|
73
|
-
const edit = edits[i];
|
|
74
|
-
rows[i] = ` ${edit.id} ${edit.path} (-${countLines(edit.oldText)}/+${countLines(edit.newText)} lines)`;
|
|
75
|
-
}
|
|
76
|
-
return [
|
|
77
|
-
"STAGED_EDITS (apply by id with apply_edits; do NOT re-type content):",
|
|
78
|
-
...rows,
|
|
79
|
-
].join("\n");
|
|
80
56
|
}
|
|
81
57
|
|
|
82
58
|
/**
|
|
83
|
-
* Assemble the model-visible text for a repl() result: cap stdout
|
|
84
|
-
* delegation nudge
|
|
85
|
-
* without exposing oldText/newText bodies to the root model.
|
|
59
|
+
* Assemble the model-visible text for a repl() result: cap stdout and append a
|
|
60
|
+
* zero-subcall delegation nudge when a bulk read went undelegated.
|
|
86
61
|
*/
|
|
87
62
|
export function buildReplResultText(
|
|
88
63
|
stdout: string,
|
|
89
64
|
finalAnswer: string | undefined,
|
|
90
|
-
edits: readonly ProposedEdit[],
|
|
91
|
-
raised: boolean,
|
|
92
65
|
subcalls: readonly RlmSubcall[],
|
|
93
66
|
): ReplResultText {
|
|
94
67
|
const answerSubmitted = finalAnswer !== undefined;
|
|
95
68
|
const rawText = answerSubmitted
|
|
96
69
|
? `ANSWER_SUBMITTED (${finalAnswer.length} chars) — delivered to user. Do not restate it.`
|
|
97
70
|
: stdout || "(no output)";
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const modelText = rawText + editsBlock;
|
|
101
|
-
// Model-visible text is capped; the caller keeps full stdout/final answer in `details` for the TUI.
|
|
102
|
-
const cappedText = capReplResultText(modelText) ?? modelText;
|
|
71
|
+
// Model-visible text is capped; the caller keeps full stdout in `details` for the TUI.
|
|
72
|
+
const cappedText = capReplResultText(rawText) ?? rawText;
|
|
103
73
|
const delegated = subcalls.some((s) => s.kind === "llm" || s.kind === "batch" || s.kind === "rlm");
|
|
104
|
-
const nudge =
|
|
105
|
-
return { text: cappedText + (nudge ?? "")
|
|
74
|
+
const nudge = answerSubmitted ? undefined : replDelegationNudge(rawText.length, delegated);
|
|
75
|
+
return { text: cappedText + (nudge ?? "") };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Advisory diagnostics derived from a completed invocation's sub-calls. */
|
|
79
|
+
export function collectReplWarnings(subcalls: readonly RlmSubcall[]): readonly string[] | undefined {
|
|
80
|
+
let failed = 0;
|
|
81
|
+
let total = 0;
|
|
82
|
+
for (let i = 0; i < subcalls.length; i++) {
|
|
83
|
+
const call = subcalls[i];
|
|
84
|
+
if (call.status !== "error") continue;
|
|
85
|
+
// A batch subcall stands for many prompts; a single call stands for one.
|
|
86
|
+
failed += call.failedCount ?? 1;
|
|
87
|
+
total += call.totalCount ?? 1;
|
|
88
|
+
}
|
|
89
|
+
if (failed === 0) return undefined;
|
|
90
|
+
return Object.freeze([`${failed}/${total} sub-call(s) failed — results may be incomplete`]);
|
|
106
91
|
}
|
|
107
92
|
|
|
108
93
|
// ── Mutable bridge state (handler indirection) ──
|
|
@@ -213,6 +198,7 @@ class NativeBridgeState {
|
|
|
213
198
|
if (id) state.currentEmitter?.emitSubcallUpdated({ id,
|
|
214
199
|
status: error ? "error" : "done", costUsd: cost, tokens,
|
|
215
200
|
resultPreview: previewText(out[0] ?? ""), detail: error,
|
|
201
|
+
failedCount: failed, totalCount: out.length,
|
|
216
202
|
});
|
|
217
203
|
return out;
|
|
218
204
|
},
|
|
@@ -336,18 +322,23 @@ export interface ReplToolDeps {
|
|
|
336
322
|
readonly getModel?: () => Model<Api> | undefined;
|
|
337
323
|
readonly getWorkerModel?: () => Model<Api> | undefined;
|
|
338
324
|
readonly registry: ModelRegistry;
|
|
339
|
-
readonly editRegistry?: EditRegistry;
|
|
340
325
|
readonly config: RlmConfig;
|
|
341
326
|
readonly signal?: AbortSignal;
|
|
342
327
|
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
343
328
|
readonly ensureContext?: () => Promise<void>;
|
|
329
|
+
/** Register a reset hook for sandbox death/dispose (e.g. load_library slot counter). */
|
|
330
|
+
readonly registerDiscardHook?: (reset: () => void) => void;
|
|
344
331
|
}
|
|
345
332
|
|
|
346
333
|
export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplToolParams, ReplDetails> {
|
|
347
|
-
const { sandboxManager, workerModel, registry,
|
|
334
|
+
const { sandboxManager, workerModel, registry, config, signal, onUsage } = deps;
|
|
348
335
|
const bridgeState = new NativeBridgeState();
|
|
349
336
|
|
|
350
|
-
//
|
|
337
|
+
// Late-bound cwd — getOrCreate installs handlers only at spawn; never rebuild the closure.
|
|
338
|
+
let sessionCwd = process.cwd();
|
|
339
|
+
|
|
340
|
+
// Build handlers once — llm/rlm/library use late-bound deps so the same closures stay correct
|
|
341
|
+
// across repl() calls; counter resets when the sandbox is discarded and re-spawned.
|
|
351
342
|
const llmHandlers = bridgeState.buildLlmHandlers({
|
|
352
343
|
workerModel,
|
|
353
344
|
getWorkerModel: deps.getWorkerModel,
|
|
@@ -373,6 +364,17 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
373
364
|
llmHandlers,
|
|
374
365
|
});
|
|
375
366
|
|
|
367
|
+
const libraryBundle = config.libraryLoader
|
|
368
|
+
? buildLibraryHandler({
|
|
369
|
+
getCwd: () => sessionCwd,
|
|
370
|
+
getEmitter: () => bridgeState.currentEmitter,
|
|
371
|
+
parentId: undefined,
|
|
372
|
+
signal,
|
|
373
|
+
startIndex: 1,
|
|
374
|
+
})
|
|
375
|
+
: undefined;
|
|
376
|
+
if (libraryBundle) deps.registerDiscardHook?.(libraryBundle.reset);
|
|
377
|
+
|
|
376
378
|
return {
|
|
377
379
|
name: "repl",
|
|
378
380
|
label: "REPL",
|
|
@@ -382,7 +384,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
382
384
|
"chunk `context` and delegate semantic work to llm_query / llm_query_batched / " +
|
|
383
385
|
"llm_query_chunked / rlm_query — stdout returned to you is hard-capped at 4K chars, so " +
|
|
384
386
|
"printing file bodies is useless. Variables, imports, and state persist across calls. " +
|
|
385
|
-
"Also supports todo and
|
|
387
|
+
"Also supports todo, ask_user_question, and load_library inside the sandbox.",
|
|
386
388
|
parameters: ReplToolParams,
|
|
387
389
|
|
|
388
390
|
async execute(_toolCallId, rawParams, _execSignal, onUpdate, ctx) {
|
|
@@ -448,12 +450,15 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
448
450
|
parentId: undefined,
|
|
449
451
|
});
|
|
450
452
|
|
|
453
|
+
sessionCwd = ctx.cwd ?? process.cwd();
|
|
454
|
+
|
|
451
455
|
await deps.ensureContext?.();
|
|
452
456
|
await sandboxManager.getOrCreate({
|
|
453
457
|
...llmHandlers,
|
|
454
458
|
...rlmHandlers,
|
|
455
459
|
askUserQuestion: interactiveHandlers.askUserQuestion,
|
|
456
460
|
todo: interactiveHandlers.todo,
|
|
461
|
+
...(libraryBundle?.handlers ?? {}),
|
|
457
462
|
});
|
|
458
463
|
|
|
459
464
|
// Detect queue contention AFTER sandbox init (initPromise settled, isExecuting now accurate)
|
|
@@ -487,14 +492,11 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
487
492
|
if (queuedId) emitter.emitSubcallUpdated({ id: queuedId, status: "done" });
|
|
488
493
|
|
|
489
494
|
const finalAnswer = result.finalAnswer ?? undefined;
|
|
490
|
-
const { text: resultText
|
|
495
|
+
const { text: resultText } = buildReplResultText(
|
|
491
496
|
result.stdout,
|
|
492
497
|
finalAnswer,
|
|
493
|
-
result.edits,
|
|
494
|
-
result.raised,
|
|
495
498
|
store.getSubcalls(),
|
|
496
499
|
);
|
|
497
|
-
editRegistry?.registerAll(surfacedEdits);
|
|
498
500
|
|
|
499
501
|
const details: ReplDetails = {
|
|
500
502
|
status: "done",
|
|
@@ -504,7 +506,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
504
506
|
subcalls: store.getSubcalls(),
|
|
505
507
|
totals: store.getTotals(),
|
|
506
508
|
finalAnswer,
|
|
507
|
-
|
|
509
|
+
warnings: collectReplWarnings(store.getSubcalls()),
|
|
508
510
|
};
|
|
509
511
|
const progressText = finalAnswer !== undefined
|
|
510
512
|
? `ANSWER_SUBMITTED (${finalAnswer.length} chars)`
|
|
@@ -567,9 +569,6 @@ function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
|
567
569
|
parts.push(formatCost(details.totals.costUsd));
|
|
568
570
|
if (details.totals.tokens > 0) parts.push(`${formatTokens(details.totals.tokens)} tok`);
|
|
569
571
|
if (details.executionTimeMs > 0) parts.push(`${details.executionTimeMs}ms`);
|
|
570
|
-
if (details.edits && details.edits.length > 0) {
|
|
571
|
-
parts.push(theme.fg("success", `${details.edits.length} staged`));
|
|
572
|
-
}
|
|
573
572
|
const stats = parts.length > 0 ? ` ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
574
573
|
|
|
575
574
|
const header = `${glyph} ${theme.fg("toolTitle", theme.bold("REPL"))}${stats}`;
|
|
@@ -604,13 +603,9 @@ function renderReplExpanded(details: ReplDetails, theme: Theme): Container {
|
|
|
604
603
|
container.addChild(new Text(out, 0, 0));
|
|
605
604
|
}
|
|
606
605
|
|
|
607
|
-
if (details.
|
|
608
|
-
const editFiles = new Set<string>();
|
|
609
|
-
for (const edit of details.edits) editFiles.add(edit.path);
|
|
606
|
+
if (details.warnings && details.warnings.length > 0) {
|
|
610
607
|
container.addChild(new Spacer(1));
|
|
611
|
-
container.addChild(new Text(theme.fg("
|
|
612
|
-
`${details.edits.length} edit${details.edits.length > 1 ? "s" : ""} staged across ${editFiles.size} file${editFiles.size > 1 ? "s" : ""}`,
|
|
613
|
-
), 0, 0));
|
|
608
|
+
container.addChild(new Text(theme.fg("muted", details.warnings.join("\n")), 0, 0));
|
|
614
609
|
}
|
|
615
610
|
|
|
616
611
|
// Stderr
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* getState(): RlmDetails for direct access (spinner loop, final return).
|
|
7
7
|
*
|
|
8
8
|
* Subcall storage and totals are delegated to SubcallStore. Root-level state
|
|
9
|
-
* (status, prompt, turns, answer,
|
|
9
|
+
* (status, prompt, turns, answer, warnings) is kept in the aggregator.
|
|
10
10
|
*
|
|
11
11
|
* Replaces RlmToolBridge's internal state accumulation. The emitter is pure
|
|
12
12
|
* dispatch; the aggregator is pure state. Separated for independent testing.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
|
|
16
|
-
import type { RlmEmitter, TurnEvent, RootUsageEvent, AnswerEvent,
|
|
16
|
+
import type { RlmEmitter, TurnEvent, RootUsageEvent, AnswerEvent, StatusEvent, RootPromptEvent, WarningsEvent } from "./rlm-events.ts";
|
|
17
17
|
import type { RlmDetails, RlmRunStatus } from "./rlm-details.ts";
|
|
18
18
|
import { EmitterListener } from "./emitter-listener.ts";
|
|
19
19
|
import { SubcallStore } from "./subcall-store.ts";
|
|
@@ -27,7 +27,7 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
27
27
|
private turnCurrent = 0;
|
|
28
28
|
private turnMax = 0;
|
|
29
29
|
private answer?: string;
|
|
30
|
-
private
|
|
30
|
+
private warnings?: readonly string[];
|
|
31
31
|
|
|
32
32
|
constructor(
|
|
33
33
|
emitter: RlmEmitter,
|
|
@@ -40,7 +40,7 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
40
40
|
emitter.onTurn((e) => this.handleTurn(e)),
|
|
41
41
|
emitter.onRootUsage((e) => this.handleRootUsage(e)),
|
|
42
42
|
emitter.onAnswer((e) => this.handleAnswer(e)),
|
|
43
|
-
emitter.
|
|
43
|
+
emitter.onWarnings((e) => this.handleWarnings(e)),
|
|
44
44
|
emitter.onStatus((e) => this.handleStatus(e)),
|
|
45
45
|
emitter.onRootPrompt((e) => this.handleRootPrompt(e)),
|
|
46
46
|
]);
|
|
@@ -64,8 +64,8 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
64
64
|
this.notify();
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
private
|
|
68
|
-
this.
|
|
67
|
+
private handleWarnings(event: WarningsEvent): void {
|
|
68
|
+
this.warnings = event.warnings;
|
|
69
69
|
this.notify();
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -90,7 +90,7 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
90
90
|
subcalls: this.store.getSubcalls(),
|
|
91
91
|
totals: this.store.getTotals(),
|
|
92
92
|
answer: this.answer,
|
|
93
|
-
|
|
93
|
+
warnings: this.warnings,
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
|
package/src/tool/rlm-details.ts
CHANGED
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
* after every mutation, enabling Pi's built-in progressive TUI re-render.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { ProposedEdit } from "../sandbox/protocol.ts";
|
|
10
|
-
|
|
11
9
|
export type SubcallKind = "root" | "rlm" | "llm" | "batch" | "tool";
|
|
12
10
|
export type SubcallStatus = "running" | "done" | "error";
|
|
13
11
|
export type RlmRunStatus = "running" | "done" | "error" | "aborted";
|
|
@@ -29,6 +27,10 @@ export interface RlmSubcall {
|
|
|
29
27
|
readonly endedAt?: number;
|
|
30
28
|
readonly costUsd: number;
|
|
31
29
|
readonly tokens: number;
|
|
30
|
+
/** For batch subcalls: failed prompt count (partial failure). */
|
|
31
|
+
readonly failedCount?: number;
|
|
32
|
+
/** For batch subcalls: total prompt count. */
|
|
33
|
+
readonly totalCount?: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export interface RlmDetails {
|
|
@@ -38,7 +40,8 @@ export interface RlmDetails {
|
|
|
38
40
|
readonly subcalls: readonly RlmSubcall[];
|
|
39
41
|
readonly totals: { readonly costUsd: number; readonly tokens: number };
|
|
40
42
|
readonly answer?: string;
|
|
41
|
-
|
|
43
|
+
/** Advisory diagnostics — surfaced to the user, never a failure. */
|
|
44
|
+
readonly warnings?: readonly string[];
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
export interface SubcallInit {
|
package/src/tool/rlm-events.ts
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
import { EventEmitter } from "node:events";
|
|
14
14
|
import type { SubcallKind, SubcallStatus, RlmRunStatus } from "./rlm-details.ts";
|
|
15
|
-
import type { ProposedEdit } from "../sandbox/protocol.ts";
|
|
16
15
|
|
|
17
16
|
// ── Event payloads ──
|
|
18
17
|
|
|
@@ -40,6 +39,14 @@ export interface SubcallUpdatedEvent {
|
|
|
40
39
|
readonly costUsd?: number;
|
|
41
40
|
/** Delta — additive on both the subcall and running totals. */
|
|
42
41
|
readonly tokens?: number;
|
|
42
|
+
/** For batch subcalls: failed prompt count. */
|
|
43
|
+
readonly failedCount?: number;
|
|
44
|
+
/** For batch subcalls: total prompt count. */
|
|
45
|
+
readonly totalCount?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface WarningsEvent {
|
|
49
|
+
readonly warnings: readonly string[];
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
export interface TurnEvent {
|
|
@@ -56,10 +63,6 @@ export interface AnswerEvent {
|
|
|
56
63
|
readonly text: string;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
export interface EditsEvent {
|
|
60
|
-
readonly edits: readonly ProposedEdit[];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
66
|
export interface StatusEvent {
|
|
64
67
|
readonly status: RlmRunStatus;
|
|
65
68
|
}
|
|
@@ -110,9 +113,9 @@ export class RlmEmitter {
|
|
|
110
113
|
this.ee.emit("answer", { text } satisfies AnswerEvent);
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
/** Set
|
|
114
|
-
|
|
115
|
-
this.ee.emit("
|
|
116
|
+
/** Set advisory warnings (root-only; never a failure). */
|
|
117
|
+
emitWarnings(warnings: readonly string[]): void {
|
|
118
|
+
this.ee.emit("warnings", { warnings } satisfies WarningsEvent);
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
/** Set the root run status (done/error/aborted). */
|
|
@@ -152,9 +155,9 @@ export class RlmEmitter {
|
|
|
152
155
|
return () => { this.ee.off("answer", handler); };
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
this.ee.on("
|
|
157
|
-
return () => { this.ee.off("
|
|
158
|
+
onWarnings(handler: (event: WarningsEvent) => void): () => void {
|
|
159
|
+
this.ee.on("warnings", handler);
|
|
160
|
+
return () => { this.ee.off("warnings", handler); };
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
onStatus(handler: (event: StatusEvent) => void): () => void {
|
package/src/tool/rlm-tool.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RLM tool — registers the RLM engine as a Pi tool with inline rendering.
|
|
3
3
|
*
|
|
4
|
-
* Modeled after rpiv-mono's subagent tool.
|
|
5
4
|
* The tool's execute() wraps createEngine() with an RlmEmitter + RlmEventAggregator that feeds
|
|
6
5
|
* onUpdate(partialResult) for progressive TUI re-rendering.
|
|
7
6
|
*/
|
|
@@ -157,14 +156,9 @@ function renderExpanded(details: RlmDetails, theme: Theme): Component {
|
|
|
157
156
|
container.addChild(new Markdown(details.answer, 0, 0, getMarkdownTheme()));
|
|
158
157
|
}
|
|
159
158
|
|
|
160
|
-
if (details.
|
|
159
|
+
if (details.warnings && details.warnings.length > 0) {
|
|
161
160
|
container.addChild(new Spacer(1));
|
|
162
|
-
|
|
163
|
-
container.addChild(new Text(
|
|
164
|
-
theme.fg("muted", "─── Edits ───") +
|
|
165
|
-
`\n ${theme.fg("dim", `${details.edits.length} edit${details.edits.length > 1 ? "s" : ""} proposed across ${editFiles.size} file${editFiles.size > 1 ? "s" : ""}`)}`,
|
|
166
|
-
0, 0,
|
|
167
|
-
));
|
|
161
|
+
container.addChild(new Text(theme.fg("muted", details.warnings.join("\n")), 0, 0));
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
return container;
|
|
@@ -66,6 +66,8 @@ export class SubcallStore extends EmitterListener {
|
|
|
66
66
|
sc.tokens += event.tokens;
|
|
67
67
|
this.totalTokens += event.tokens;
|
|
68
68
|
}
|
|
69
|
+
if (event.failedCount !== undefined) sc.failedCount = event.failedCount;
|
|
70
|
+
if (event.totalCount !== undefined) sc.totalCount = event.totalCount;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
// ── Read ──
|
package/src/ui/config-panel.ts
CHANGED
|
@@ -16,11 +16,13 @@ const CHOICES = Object.freeze({
|
|
|
16
16
|
maxErrors: Object.freeze(["3", "5", "10", "none"]),
|
|
17
17
|
orchestrator: Object.freeze(["on", "off"]),
|
|
18
18
|
pipeline: Object.freeze(["on", "off"]),
|
|
19
|
+
maxBackwardJumps: Object.freeze(["0", "1", "2", "3"]),
|
|
19
20
|
compaction: Object.freeze(["on", "off"]),
|
|
20
21
|
rootSamplingMaxTokens: Object.freeze(["4096", "8192", "16384", "32768"]),
|
|
21
22
|
sandboxInitTimeoutMs: Object.freeze(["10000", "30000", "60000", "120000"]),
|
|
22
23
|
askUserQuestion: Object.freeze(["on", "off"]),
|
|
23
24
|
todo: Object.freeze(["on", "off"]),
|
|
25
|
+
libraryLoader: Object.freeze(["on", "off"]),
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
function item(id: string, label: string, currentValue: string, values: readonly string[], description: string): SettingItem {
|
|
@@ -39,12 +41,15 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
39
41
|
item("maxTokens", "Token ceiling", config.maxTokens != null ? String(config.maxTokens) : "none", CHOICES.maxTokens, "Total input+output token cap for the whole recursive tree."),
|
|
40
42
|
item("maxErrors", "Max consecutive errors", config.maxErrors != null ? String(config.maxErrors) : "none", CHOICES.maxErrors, "Stop after this many consecutive failing turns; none disables the guard."),
|
|
41
43
|
item("orchestrator", "Orchestrator addendum", config.orchestrator ? "on" : "off", CHOICES.orchestrator, "Append extra divide-and-conquer guidance to the root model system prompt."),
|
|
42
|
-
item("pipeline", "Phase pipeline", config.pipeline ? "on" : "off", CHOICES.pipeline, "Enable
|
|
44
|
+
item("pipeline", "Phase pipeline", config.pipeline ? "on" : "off", CHOICES.pipeline, "Enable artifact-gated phases: clarify→research→blueprint→validate (read-only plan pipeline; clarify needs Ask user on)."),
|
|
45
|
+
item("maxBackwardJumps", "Max validate→blueprint loops", String(config.maxBackwardJumps), CHOICES.maxBackwardJumps, "Bounded corrective re-entries when validation reports blockers_count > 0."),
|
|
43
46
|
item("compaction", "Trajectory compaction", config.compaction ? "on" : "off", CHOICES.compaction, "Summarize old turns when history approaches the model context window."),
|
|
44
47
|
item("rootSamplingMaxTokens", "Root model output cap (tok)", String(config.rootSampling?.maxTokens ?? 16384), CHOICES.rootSamplingMaxTokens, "Max output tokens per root-model turn. Lower values keep each turn lean."),
|
|
45
48
|
item("sandboxInitTimeoutMs", "Sandbox init timeout", String(config.sandboxInitTimeoutMs), CHOICES.sandboxInitTimeoutMs, "How long to wait for the Python worker to start."),
|
|
46
49
|
item("askUserQuestion", "[Interactive] Ask user", config.askUserQuestion ? "on" : "off", CHOICES.askUserQuestion, "Allow root REPL code to present structured ask_user_question dialogs."),
|
|
47
50
|
item("todo", "[Interactive] Todo", config.todo ? "on" : "off", CHOICES.todo, "Allow REPL code to manage a visible todo task list."),
|
|
51
|
+
item("libraryLoader", "Library loader", config.libraryLoader ? "on" : "off", CHOICES.libraryLoader,
|
|
52
|
+
"Allow load_library() to pull an external dir, file, or git repo into the shared context list."),
|
|
48
53
|
item("__save__", "Save & close", "↵", ["↵"], "Save these settings and close (Esc also saves)."),
|
|
49
54
|
];
|
|
50
55
|
|
|
@@ -86,10 +91,12 @@ function applySetting(config: RlmConfig, id: string, value: string): void {
|
|
|
86
91
|
case "maxErrors": config.maxErrors = value === "none" ? undefined : Number(value); break;
|
|
87
92
|
case "orchestrator": config.orchestrator = value === "on"; break;
|
|
88
93
|
case "pipeline": config.pipeline = value === "on"; break;
|
|
94
|
+
case "maxBackwardJumps": config.maxBackwardJumps = Number(value); break;
|
|
89
95
|
case "compaction": config.compaction = value === "on"; break;
|
|
90
96
|
case "rootSamplingMaxTokens": config.rootSampling = Object.freeze({ ...config.rootSampling, maxTokens: Number(value) }); break;
|
|
91
97
|
case "sandboxInitTimeoutMs": config.sandboxInitTimeoutMs = Number(value); break;
|
|
92
98
|
case "askUserQuestion": config.askUserQuestion = value === "on"; break;
|
|
93
99
|
case "todo": config.todo = value === "on"; break;
|
|
100
|
+
case "libraryLoader": config.libraryLoader = value === "on"; break;
|
|
94
101
|
}
|
|
95
102
|
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { ProposedEdit } from "../sandbox/protocol.ts";
|
|
2
|
-
|
|
3
|
-
export class EditRegistry {
|
|
4
|
-
private readonly edits = new Map<string, ProposedEdit>();
|
|
5
|
-
|
|
6
|
-
registerAll(edits: readonly ProposedEdit[] | undefined): void {
|
|
7
|
-
if (edits === undefined) return;
|
|
8
|
-
for (const edit of edits) this.edits.set(edit.id, edit);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
get(id: string): ProposedEdit | undefined {
|
|
12
|
-
return this.edits.get(id);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
delete(id: string): boolean {
|
|
16
|
-
return this.edits.delete(id);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
clear(): void {
|
|
20
|
-
this.edits.clear();
|
|
21
|
-
}
|
|
22
|
-
}
|
package/src/text/edits.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface AnchorEdit {
|
|
2
|
-
readonly oldText: string;
|
|
3
|
-
readonly newText: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function countOccurrences(haystack: string, needle: string): number {
|
|
7
|
-
if (needle.length === 0) return 0;
|
|
8
|
-
let count = 0;
|
|
9
|
-
let offset = 0;
|
|
10
|
-
for (;;) {
|
|
11
|
-
const match = haystack.indexOf(needle, offset);
|
|
12
|
-
if (match < 0) return count;
|
|
13
|
-
count++;
|
|
14
|
-
offset = match + needle.length;
|
|
15
|
-
}
|
|
16
|
-
}
|