@hicaru/pi-rlm 0.1.8 → 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 +22 -19
- package/package.json +2 -1
- package/src/bridge/library.ts +93 -15
- package/src/bridge/llm-query.ts +1 -0
- package/src/bridge/rlm-query.ts +2 -4
- package/src/context/library-context.ts +209 -22
- package/src/context/repomix-context.ts +2 -48
- package/src/core/answer.ts +1 -10
- package/src/core/critique.ts +92 -0
- package/src/core/engine.ts +130 -134
- package/src/core/gates.ts +30 -1
- package/src/core/pipeline.ts +38 -13
- package/src/core/types.ts +1 -3
- package/src/index.ts +1 -9
- package/src/mode/native-guards.ts +2 -2
- package/src/prompts/phases.ts +18 -39
- package/src/prompts/system.ts +31 -19
- package/src/sandbox/protocol.ts +5 -10
- package/src/sandbox/sandbox.ts +43 -9
- package/src/sandbox/worker.py +180 -48
- package/src/state/resume.ts +21 -14
- package/src/state/rows.ts +2 -2
- package/src/text/tokens.ts +7 -1
- package/src/tool/repl-details.ts +2 -3
- package/src/tool/repl-tool.ts +29 -55
- 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 -7
- package/src/tool/subcall-store.ts +2 -0
- package/src/ui/config-panel.ts +2 -2
- 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
|
@@ -28,14 +28,13 @@ import { checkResourceLimits } from "../core/resource-limits.ts";
|
|
|
28
28
|
import type { InteractiveDeps, RlmConfig, Sampling } from "../core/types.ts";
|
|
29
29
|
import { SandboxManager } from "../sandbox/sandbox-manager.ts";
|
|
30
30
|
import type { SubLlmHandlers } from "../sandbox/sandbox.ts";
|
|
31
|
-
import type {
|
|
31
|
+
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
32
32
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
33
33
|
import { SubcallStore } from "./subcall-store.ts";
|
|
34
34
|
import type { ReplDetails } from "./repl-details.ts";
|
|
35
35
|
import type { RlmSubcall } from "./rlm-details.ts";
|
|
36
36
|
import { createEngine } from "../core/engine.ts";
|
|
37
37
|
import { formatCost, formatTokens, spinnerFrame } from "../ui/theme.ts";
|
|
38
|
-
import type { EditRegistry } from "../registry/edit-registry.ts";
|
|
39
38
|
import { errorMessage, formatError, isErrorText } from "../util/errors.ts";
|
|
40
39
|
import {
|
|
41
40
|
headlineStatusGlyph,
|
|
@@ -51,59 +50,44 @@ export const ReplToolParams = Object.freeze(Type.Object({
|
|
|
51
50
|
code: Type.String({ description: "Python code to execute in the persistent REPL sandbox" }),
|
|
52
51
|
}));
|
|
53
52
|
|
|
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`. */
|
|
53
|
+
/** Model-visible text assembled from a repl() result. */
|
|
59
54
|
export interface ReplResultText {
|
|
60
55
|
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
56
|
}
|
|
82
57
|
|
|
83
58
|
/**
|
|
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.
|
|
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.
|
|
87
61
|
*/
|
|
88
62
|
export function buildReplResultText(
|
|
89
63
|
stdout: string,
|
|
90
64
|
finalAnswer: string | undefined,
|
|
91
|
-
edits: readonly ProposedEdit[],
|
|
92
|
-
raised: boolean,
|
|
93
65
|
subcalls: readonly RlmSubcall[],
|
|
94
66
|
): ReplResultText {
|
|
95
67
|
const answerSubmitted = finalAnswer !== undefined;
|
|
96
68
|
const rawText = answerSubmitted
|
|
97
69
|
? `ANSWER_SUBMITTED (${finalAnswer.length} chars) — delivered to user. Do not restate it.`
|
|
98
70
|
: 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;
|
|
71
|
+
// Model-visible text is capped; the caller keeps full stdout in `details` for the TUI.
|
|
72
|
+
const cappedText = capReplResultText(rawText) ?? rawText;
|
|
104
73
|
const delegated = subcalls.some((s) => s.kind === "llm" || s.kind === "batch" || s.kind === "rlm");
|
|
105
|
-
const nudge =
|
|
106
|
-
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`]);
|
|
107
91
|
}
|
|
108
92
|
|
|
109
93
|
// ── Mutable bridge state (handler indirection) ──
|
|
@@ -214,6 +198,7 @@ class NativeBridgeState {
|
|
|
214
198
|
if (id) state.currentEmitter?.emitSubcallUpdated({ id,
|
|
215
199
|
status: error ? "error" : "done", costUsd: cost, tokens,
|
|
216
200
|
resultPreview: previewText(out[0] ?? ""), detail: error,
|
|
201
|
+
failedCount: failed, totalCount: out.length,
|
|
217
202
|
});
|
|
218
203
|
return out;
|
|
219
204
|
},
|
|
@@ -337,7 +322,6 @@ export interface ReplToolDeps {
|
|
|
337
322
|
readonly getModel?: () => Model<Api> | undefined;
|
|
338
323
|
readonly getWorkerModel?: () => Model<Api> | undefined;
|
|
339
324
|
readonly registry: ModelRegistry;
|
|
340
|
-
readonly editRegistry?: EditRegistry;
|
|
341
325
|
readonly config: RlmConfig;
|
|
342
326
|
readonly signal?: AbortSignal;
|
|
343
327
|
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
@@ -347,7 +331,7 @@ export interface ReplToolDeps {
|
|
|
347
331
|
}
|
|
348
332
|
|
|
349
333
|
export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplToolParams, ReplDetails> {
|
|
350
|
-
const { sandboxManager, workerModel, registry,
|
|
334
|
+
const { sandboxManager, workerModel, registry, config, signal, onUsage } = deps;
|
|
351
335
|
const bridgeState = new NativeBridgeState();
|
|
352
336
|
|
|
353
337
|
// Late-bound cwd — getOrCreate installs handlers only at spawn; never rebuild the closure.
|
|
@@ -508,14 +492,11 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
508
492
|
if (queuedId) emitter.emitSubcallUpdated({ id: queuedId, status: "done" });
|
|
509
493
|
|
|
510
494
|
const finalAnswer = result.finalAnswer ?? undefined;
|
|
511
|
-
const { text: resultText
|
|
495
|
+
const { text: resultText } = buildReplResultText(
|
|
512
496
|
result.stdout,
|
|
513
497
|
finalAnswer,
|
|
514
|
-
result.edits,
|
|
515
|
-
result.raised,
|
|
516
498
|
store.getSubcalls(),
|
|
517
499
|
);
|
|
518
|
-
editRegistry?.registerAll(surfacedEdits);
|
|
519
500
|
|
|
520
501
|
const details: ReplDetails = {
|
|
521
502
|
status: "done",
|
|
@@ -525,7 +506,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
525
506
|
subcalls: store.getSubcalls(),
|
|
526
507
|
totals: store.getTotals(),
|
|
527
508
|
finalAnswer,
|
|
528
|
-
|
|
509
|
+
warnings: collectReplWarnings(store.getSubcalls()),
|
|
529
510
|
};
|
|
530
511
|
const progressText = finalAnswer !== undefined
|
|
531
512
|
? `ANSWER_SUBMITTED (${finalAnswer.length} chars)`
|
|
@@ -588,9 +569,6 @@ function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
|
588
569
|
parts.push(formatCost(details.totals.costUsd));
|
|
589
570
|
if (details.totals.tokens > 0) parts.push(`${formatTokens(details.totals.tokens)} tok`);
|
|
590
571
|
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
572
|
const stats = parts.length > 0 ? ` ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
595
573
|
|
|
596
574
|
const header = `${glyph} ${theme.fg("toolTitle", theme.bold("REPL"))}${stats}`;
|
|
@@ -625,13 +603,9 @@ function renderReplExpanded(details: ReplDetails, theme: Theme): Container {
|
|
|
625
603
|
container.addChild(new Text(out, 0, 0));
|
|
626
604
|
}
|
|
627
605
|
|
|
628
|
-
if (details.
|
|
629
|
-
const editFiles = new Set<string>();
|
|
630
|
-
for (const edit of details.edits) editFiles.add(edit.path);
|
|
606
|
+
if (details.warnings && details.warnings.length > 0) {
|
|
631
607
|
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));
|
|
608
|
+
container.addChild(new Text(theme.fg("muted", details.warnings.join("\n")), 0, 0));
|
|
635
609
|
}
|
|
636
610
|
|
|
637
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
|
@@ -156,14 +156,9 @@ function renderExpanded(details: RlmDetails, theme: Theme): Component {
|
|
|
156
156
|
container.addChild(new Markdown(details.answer, 0, 0, getMarkdownTheme()));
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
if (details.
|
|
159
|
+
if (details.warnings && details.warnings.length > 0) {
|
|
160
160
|
container.addChild(new Spacer(1));
|
|
161
|
-
|
|
162
|
-
container.addChild(new Text(
|
|
163
|
-
theme.fg("muted", "─── Edits ───") +
|
|
164
|
-
`\n ${theme.fg("dim", `${details.edits.length} edit${details.edits.length > 1 ? "s" : ""} proposed across ${editFiles.size} file${editFiles.size > 1 ? "s" : ""}`)}`,
|
|
165
|
-
0, 0,
|
|
166
|
-
));
|
|
161
|
+
container.addChild(new Text(theme.fg("muted", details.warnings.join("\n")), 0, 0));
|
|
167
162
|
}
|
|
168
163
|
|
|
169
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
|
@@ -41,7 +41,7 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
41
41
|
item("maxTokens", "Token ceiling", config.maxTokens != null ? String(config.maxTokens) : "none", CHOICES.maxTokens, "Total input+output token cap for the whole recursive tree."),
|
|
42
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."),
|
|
43
43
|
item("orchestrator", "Orchestrator addendum", config.orchestrator ? "on" : "off", CHOICES.orchestrator, "Append extra divide-and-conquer guidance to the root model system prompt."),
|
|
44
|
-
item("pipeline", "Phase pipeline", config.pipeline ? "on" : "off", CHOICES.pipeline, "Enable artifact-gated phases: clarify→research→blueprint→
|
|
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
45
|
item("maxBackwardJumps", "Max validate→blueprint loops", String(config.maxBackwardJumps), CHOICES.maxBackwardJumps, "Bounded corrective re-entries when validation reports blockers_count > 0."),
|
|
46
46
|
item("compaction", "Trajectory compaction", config.compaction ? "on" : "off", CHOICES.compaction, "Summarize old turns when history approaches the model context window."),
|
|
47
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."),
|
|
@@ -49,7 +49,7 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
49
49
|
item("askUserQuestion", "[Interactive] Ask user", config.askUserQuestion ? "on" : "off", CHOICES.askUserQuestion, "Allow root REPL code to present structured ask_user_question dialogs."),
|
|
50
50
|
item("todo", "[Interactive] Todo", config.todo ? "on" : "off", CHOICES.todo, "Allow REPL code to manage a visible todo task list."),
|
|
51
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
|
|
52
|
+
"Allow load_library() to pull an external dir, file, or git repo into the shared context list."),
|
|
53
53
|
item("__save__", "Save & close", "↵", ["↵"], "Save these settings and close (Esc also saves)."),
|
|
54
54
|
];
|
|
55
55
|
|
|
@@ -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,164 +0,0 @@
|
|
|
1
|
-
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { constants } from "node:fs";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
-
import type { ProposedEdit } from "../sandbox/protocol.ts";
|
|
5
|
-
import { errorMessage, formatError } from "../util/errors.ts";
|
|
6
|
-
|
|
7
|
-
export interface AnchorEdit {
|
|
8
|
-
readonly oldText: string;
|
|
9
|
-
readonly newText: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function countOccurrences(haystack: string, needle: string): number {
|
|
13
|
-
if (needle.length === 0) return 0;
|
|
14
|
-
let count = 0;
|
|
15
|
-
let offset = 0;
|
|
16
|
-
for (;;) {
|
|
17
|
-
const match = haystack.indexOf(needle, offset);
|
|
18
|
-
if (match < 0) return count;
|
|
19
|
-
count++;
|
|
20
|
-
offset = match + needle.length;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Literal string replace of the first occurrence of `oldText` with `newText`.
|
|
26
|
-
* Splice-based so `$&` / `$$` / `$'` in newText are NOT treated as
|
|
27
|
-
* special replacement patterns (String.prototype.replace string-form hazard).
|
|
28
|
-
*/
|
|
29
|
-
export function replaceOnceLiteral(content: string, oldText: string, newText: string): string {
|
|
30
|
-
const idx = content.indexOf(oldText);
|
|
31
|
-
if (idx < 0) return content;
|
|
32
|
-
return content.slice(0, idx) + newText + content.slice(idx + oldText.length);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type PlanEditResult =
|
|
36
|
-
| {
|
|
37
|
-
readonly ok: true;
|
|
38
|
-
/** create = new file; replace = one-shot anchor swap; already-applied = idempotent skip */
|
|
39
|
-
readonly kind: "create" | "replace" | "already-applied";
|
|
40
|
-
readonly before: string;
|
|
41
|
-
readonly after: string;
|
|
42
|
-
}
|
|
43
|
-
| { readonly ok: false; readonly error: string };
|
|
44
|
-
|
|
45
|
-
async function pathExists(abs: string): Promise<boolean> {
|
|
46
|
-
try {
|
|
47
|
-
await access(abs, constants.F_OK);
|
|
48
|
-
return true;
|
|
49
|
-
} catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Validate and compute an edit without writing.
|
|
56
|
-
* Shared by headless apply and the native apply_edits tool (DRY).
|
|
57
|
-
*
|
|
58
|
-
* Idempotent retry: if a prior unit already landed the change
|
|
59
|
-
* (oldText absent + newText already present for replace; create with identical content),
|
|
60
|
-
* returns kind "already-applied" so a failed fanout can be retried without wedging.
|
|
61
|
-
*
|
|
62
|
-
* Create-file: refuses to clobber when the target exists with different content.
|
|
63
|
-
*/
|
|
64
|
-
export async function planEdit(
|
|
65
|
-
cwd: string,
|
|
66
|
-
path: string,
|
|
67
|
-
oldText: string,
|
|
68
|
-
newText: string,
|
|
69
|
-
): Promise<PlanEditResult> {
|
|
70
|
-
try {
|
|
71
|
-
const fullPath = resolve(cwd, path);
|
|
72
|
-
if (oldText.length === 0) {
|
|
73
|
-
if (await pathExists(fullPath)) {
|
|
74
|
-
const existing = await readFile(fullPath, "utf8");
|
|
75
|
-
if (existing === newText) {
|
|
76
|
-
return { ok: true, kind: "already-applied", before: existing, after: existing };
|
|
77
|
-
}
|
|
78
|
-
return {
|
|
79
|
-
ok: false,
|
|
80
|
-
error: formatError(
|
|
81
|
-
`${path}: file already exists with different content — refuse to clobber (create requires empty target or identical content)`,
|
|
82
|
-
),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
return { ok: true, kind: "create", before: "", after: newText };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const content = await readFile(fullPath, "utf8");
|
|
89
|
-
const occurrences = countOccurrences(content, oldText);
|
|
90
|
-
if (occurrences === 0) {
|
|
91
|
-
// Idempotent skip: prior apply already removed the anchor and left newText.
|
|
92
|
-
// Deletions (newText === "") always "include" empty string — treat them as
|
|
93
|
-
// retry-unsafe so a typo'd anchor fails instead of silently skipping.
|
|
94
|
-
if (newText.length > 0 && content.includes(newText)) {
|
|
95
|
-
return { ok: true, kind: "already-applied", before: content, after: content };
|
|
96
|
-
}
|
|
97
|
-
return { ok: false, error: formatError(`anchor occurs 0 times in ${path}`) };
|
|
98
|
-
}
|
|
99
|
-
if (occurrences !== 1) {
|
|
100
|
-
return { ok: false, error: formatError(`anchor occurs ${occurrences} times in ${path}`) };
|
|
101
|
-
}
|
|
102
|
-
const after = replaceOnceLiteral(content, oldText, newText);
|
|
103
|
-
return { ok: true, kind: "replace", before: content, after };
|
|
104
|
-
} catch (err: unknown) {
|
|
105
|
-
return { ok: false, error: formatError(`${path}: ${errorMessage(err)}`) };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export type ApplyOneEditResult =
|
|
110
|
-
| { readonly ok: true; readonly before: string; readonly after: string; readonly kind: "create" | "replace" | "already-applied" }
|
|
111
|
-
| { readonly ok: false; readonly error: string };
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Apply a single anchor edit to the working tree (direct disk write).
|
|
115
|
-
* Used by the implement fanout and any headless apply path.
|
|
116
|
-
*/
|
|
117
|
-
export async function applyOneEdit(
|
|
118
|
-
cwd: string,
|
|
119
|
-
path: string,
|
|
120
|
-
oldText: string,
|
|
121
|
-
newText: string,
|
|
122
|
-
): Promise<ApplyOneEditResult> {
|
|
123
|
-
const planned = await planEdit(cwd, path, oldText, newText);
|
|
124
|
-
if (!planned.ok) return planned;
|
|
125
|
-
if (planned.kind === "already-applied") {
|
|
126
|
-
return { ok: true, before: planned.before, after: planned.after, kind: "already-applied" };
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
const fullPath = resolve(cwd, path);
|
|
130
|
-
if (planned.kind === "create") {
|
|
131
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
132
|
-
}
|
|
133
|
-
await writeFile(fullPath, planned.after, "utf8");
|
|
134
|
-
return { ok: true, before: planned.before, after: planned.after, kind: planned.kind };
|
|
135
|
-
} catch (err: unknown) {
|
|
136
|
-
return { ok: false, error: formatError(`${path}: ${errorMessage(err)}`) };
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export type ApplyProposedEditsResult =
|
|
141
|
-
| { readonly ok: true; readonly applied: number }
|
|
142
|
-
| { readonly ok: false; readonly error: string; readonly applied: number };
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Apply a series of proposed edits to the working tree (patch series, not a race).
|
|
146
|
-
* Shared by the implement fanout and any headless apply path.
|
|
147
|
-
* Safe to re-run: already-applied units succeed without re-writing.
|
|
148
|
-
*/
|
|
149
|
-
export async function applyProposedEdits(
|
|
150
|
-
edits: readonly ProposedEdit[],
|
|
151
|
-
cwd: string,
|
|
152
|
-
): Promise<ApplyProposedEditsResult> {
|
|
153
|
-
let applied = 0;
|
|
154
|
-
for (let i = 0; i < edits.length; i++) {
|
|
155
|
-
const edit = edits[i];
|
|
156
|
-
if (edit === undefined) continue;
|
|
157
|
-
const one = await applyOneEdit(cwd, edit.path, edit.oldText, edit.newText);
|
|
158
|
-
if (!one.ok) {
|
|
159
|
-
return { ok: false, error: one.error, applied };
|
|
160
|
-
}
|
|
161
|
-
applied++;
|
|
162
|
-
}
|
|
163
|
-
return { ok: true, applied };
|
|
164
|
-
}
|