@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
|
@@ -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,16 +40,7 @@ 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
|
-
export interface SubcallInit {
|
|
45
|
-
readonly parentId?: string;
|
|
46
|
-
readonly kind: SubcallKind;
|
|
47
|
-
readonly label: string;
|
|
48
|
-
readonly model?: string;
|
|
49
|
-
readonly detail?: string;
|
|
50
|
-
readonly args?: string;
|
|
51
|
-
/** Recursion depth. Required — all call sites pass this. */
|
|
52
|
-
readonly depth: number;
|
|
53
|
-
}
|
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
|
@@ -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,17 +149,12 @@ 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
|
-
if (details.
|
|
155
|
+
if (details.warnings && details.warnings.length > 0) {
|
|
160
156
|
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
|
-
));
|
|
157
|
+
container.addChild(new Text(theme.fg("muted", details.warnings.join("\n")), 0, 0));
|
|
167
158
|
}
|
|
168
159
|
|
|
169
160
|
return container;
|
|
@@ -172,14 +163,5 @@ function renderExpanded(details: RlmDetails, theme: Theme): Component {
|
|
|
172
163
|
// ── Collapsed view ──
|
|
173
164
|
|
|
174
165
|
function renderCollapsed(details: RlmDetails, theme: Theme): Text {
|
|
175
|
-
|
|
176
|
-
const header = `${glyph} ${theme.fg("toolTitle", theme.bold("RLM"))} · ${rootStats(details, theme)}`;
|
|
177
|
-
|
|
178
|
-
let body = "";
|
|
179
|
-
if (details.subcalls.length > 0) {
|
|
180
|
-
body = `\n${renderCollapsedSubcallTree(details.subcalls, theme)}`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const expandHint = details.status === "running" ? "" : `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
184
|
-
return new Text(`${header}${body}${expandHint}`, 0, 0);
|
|
166
|
+
return renderCollapsedCard("RLM", details.status, rootStats(details, theme), details.subcalls, theme);
|
|
185
167
|
}
|
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Container, Text, type Component } from "@earendil-works/pi-tui";
|
|
10
|
+
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import type { RlmSubcall, SubcallStatus } from "./rlm-details.ts";
|
|
11
12
|
import { formatCost, formatDuration, formatTokens, spinnerFrame } from "../ui/theme.ts";
|
|
13
|
+
import { previewText } from "../text/preview.ts";
|
|
12
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
/** Preview budgets for the expanded tree (args are terse, results get more room). */
|
|
17
|
+
const ARGS_PREVIEW_CHARS = 80;
|
|
18
|
+
const RESULT_PREVIEW_CHARS = 120;
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
return theme.fg("warning", spinnerFrame());
|
|
18
|
-
}
|
|
20
|
+
// ── Glyphs ──
|
|
19
21
|
|
|
20
22
|
export function subcallStatusGlyph(sc: Pick<RlmSubcall, "status">, theme: Theme): string {
|
|
21
23
|
if (sc.status === "running") return theme.fg("warning", "⏳");
|
|
@@ -38,10 +40,62 @@ export function subcallStatsLine(sc: Pick<RlmSubcall, "costUsd" | "tokens" | "en
|
|
|
38
40
|
const parts: string[] = [];
|
|
39
41
|
if (sc.costUsd > 0) parts.push(formatCost(sc.costUsd));
|
|
40
42
|
if (sc.tokens > 0) parts.push(`${formatTokens(sc.tokens)} tok`);
|
|
41
|
-
|
|
43
|
+
// Explicit undefined checks: a 0 timestamp is falsy but legitimate (fixtures, epoch clocks).
|
|
44
|
+
if (sc.endedAt !== undefined && sc.startedAt !== undefined) parts.push(formatDuration(sc.endedAt - sc.startedAt));
|
|
42
45
|
return parts.join(" · ");
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
// ── Shared card scaffolding (rlm + repl render the same shape) ──
|
|
49
|
+
|
|
50
|
+
/** The `$0.0123 · 4.2k tok · 812ms` run of a card header. Omits any zero component. */
|
|
51
|
+
export function cardStatsLine(
|
|
52
|
+
totals: { readonly costUsd: number; readonly tokens: number },
|
|
53
|
+
theme: Theme,
|
|
54
|
+
extra?: string,
|
|
55
|
+
): string {
|
|
56
|
+
const parts: string[] = [formatCost(totals.costUsd)];
|
|
57
|
+
if (totals.tokens > 0) parts.push(`${formatTokens(totals.tokens)} tok`);
|
|
58
|
+
if (extra) parts.push(extra);
|
|
59
|
+
return theme.fg("dim", parts.join(" · "));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `<glyph> <TITLE> <stats>` — the first line of both tools' collapsed and expanded views. */
|
|
63
|
+
export function cardHeader(
|
|
64
|
+
title: string,
|
|
65
|
+
status: SubcallStatus | "aborted" | "done",
|
|
66
|
+
stats: string,
|
|
67
|
+
theme: Theme,
|
|
68
|
+
): string {
|
|
69
|
+
return `${headlineStatusGlyph(status, theme)} ${theme.fg("toolTitle", theme.bold(title))} ${stats}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The expand hint, using the user's actual binding rather than a hardcoded "Ctrl+O".
|
|
74
|
+
*
|
|
75
|
+
* Deliberately `keyText` + the injected theme rather than pi's `keyHint`: `keyHint` colours via
|
|
76
|
+
* pi's module-global theme, which throws when that global is uninitialized — the same jiti
|
|
77
|
+
* hazard `ui/theme-adapter.ts` exists to avoid. `keyText` only reads the keybinding registry.
|
|
78
|
+
*/
|
|
79
|
+
function expandHint(theme: Theme): string {
|
|
80
|
+
// Empty outside a live pi session (the app installs the real binding registry at startup) —
|
|
81
|
+
// the phrase stays the same, only the key prefix drops out.
|
|
82
|
+
const key = keyText("app.tools.expand");
|
|
83
|
+
return theme.fg("muted", key ? `${key} to expand` : "to expand");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The collapsed card: header, the sub-call tree, and the expand hint. */
|
|
87
|
+
export function renderCollapsedCard(
|
|
88
|
+
title: string,
|
|
89
|
+
status: SubcallStatus | "aborted" | "done",
|
|
90
|
+
stats: string,
|
|
91
|
+
subcalls: readonly RlmSubcall[],
|
|
92
|
+
theme: Theme,
|
|
93
|
+
): Text {
|
|
94
|
+
const body = subcalls.length > 0 ? `\n${renderCollapsedSubcallTree(subcalls, theme)}` : "";
|
|
95
|
+
const hint = status === "running" ? "" : `\n${expandHint(theme)}`;
|
|
96
|
+
return new Text(`${cardHeader(title, status, stats, theme)}${body}${hint}`, 0, 0);
|
|
97
|
+
}
|
|
98
|
+
|
|
45
99
|
// ── Tree building ──
|
|
46
100
|
|
|
47
101
|
function buildParentMap(subcalls: readonly RlmSubcall[]): Map<string | undefined, RlmSubcall[]> {
|
|
@@ -104,14 +158,12 @@ export function renderExpandedSubcallTree(
|
|
|
104
158
|
let line = `${pad}${sGlyph} ${sKind}${sModel}${sStats}`;
|
|
105
159
|
|
|
106
160
|
if (sc.args) {
|
|
107
|
-
|
|
108
|
-
line += `\n${pad} ${theme.fg("dim", ap)}`;
|
|
161
|
+
line += `\n${pad} ${theme.fg("dim", previewText(sc.args, ARGS_PREVIEW_CHARS))}`;
|
|
109
162
|
}
|
|
110
163
|
if (sc.status === "error" && sc.detail) {
|
|
111
164
|
line += `\n${pad} ${theme.fg("error", `✗ ${sc.detail}`)}`;
|
|
112
165
|
} else if (sc.resultPreview) {
|
|
113
|
-
|
|
114
|
-
line += `\n${pad} ${theme.fg("toolOutput", rp)}`;
|
|
166
|
+
line += `\n${pad} ${theme.fg("toolOutput", previewText(sc.resultPreview, RESULT_PREVIEW_CHARS))}`;
|
|
115
167
|
}
|
|
116
168
|
|
|
117
169
|
container.addChild(new Text(line, 0, 0));
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* subcall accumulation logic.
|
|
8
8
|
*/
|
|
9
9
|
import type { RlmEmitter, SubcallCreatedEvent, SubcallUpdatedEvent } from "./rlm-events.ts";
|
|
10
|
-
import type { RlmSubcall
|
|
10
|
+
import type { RlmSubcall } from "./rlm-details.ts";
|
|
11
11
|
import { EmitterListener } from "./emitter-listener.ts";
|
|
12
12
|
|
|
13
13
|
type MutableSubcall = {
|
|
@@ -66,13 +66,15 @@ 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 ──
|
|
72
74
|
|
|
73
75
|
/** Snapshot subcall array. Allocates a new array from Map values. */
|
|
74
76
|
getSubcalls(): RlmSubcall[] {
|
|
75
|
-
return Array.from(this.subcalls.values(), (subcall) => Object.freeze({ ...subcall
|
|
77
|
+
return Array.from(this.subcalls.values(), (subcall) => Object.freeze({ ...subcall }));
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
/** Snapshot running totals. O(1). */
|
package/src/ui/config-panel.ts
CHANGED
|
@@ -18,8 +18,10 @@ const CHOICES = Object.freeze({
|
|
|
18
18
|
pipeline: Object.freeze(["on", "off"]),
|
|
19
19
|
maxBackwardJumps: Object.freeze(["0", "1", "2", "3"]),
|
|
20
20
|
compaction: Object.freeze(["on", "off"]),
|
|
21
|
+
compactionThresholdPct: Object.freeze(["50", "65", "80", "90"]),
|
|
21
22
|
rootSamplingMaxTokens: Object.freeze(["4096", "8192", "16384", "32768"]),
|
|
22
23
|
sandboxInitTimeoutMs: Object.freeze(["10000", "30000", "60000", "120000"]),
|
|
24
|
+
requestTimeoutMs: Object.freeze(["2", "5", "10", "20"]),
|
|
23
25
|
askUserQuestion: Object.freeze(["on", "off"]),
|
|
24
26
|
todo: Object.freeze(["on", "off"]),
|
|
25
27
|
libraryLoader: Object.freeze(["on", "off"]),
|
|
@@ -29,8 +31,13 @@ function item(id: string, label: string, currentValue: string, values: readonly
|
|
|
29
31
|
return { id, label, currentValue, values: [...values], description };
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Show the settings panel and resolve with the edited config.
|
|
36
|
+
* `config` is never mutated — each change produces a new frozen object.
|
|
37
|
+
*/
|
|
38
|
+
export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig): Promise<RlmConfig> {
|
|
39
|
+
if (ctx.mode !== "tui") return config;
|
|
40
|
+
let edited = config;
|
|
34
41
|
const items: SettingItem[] = [
|
|
35
42
|
item("maxDepth", "Max recursion depth", String(config.maxDepth), CHOICES.maxDepth, "rlm_query past this depth degrades to plain llm_query (1 = no recursion)."),
|
|
36
43
|
item("maxIterations", "Max iterations", String(config.maxIterations), CHOICES.maxIterations, "Maximum root REPL turns before RLM asks the model for a final answer."),
|
|
@@ -41,15 +48,17 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
41
48
|
item("maxTokens", "Token ceiling", config.maxTokens != null ? String(config.maxTokens) : "none", CHOICES.maxTokens, "Total input+output token cap for the whole recursive tree."),
|
|
42
49
|
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
50
|
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→
|
|
51
|
+
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
52
|
item("maxBackwardJumps", "Max validate→blueprint loops", String(config.maxBackwardJumps), CHOICES.maxBackwardJumps, "Bounded corrective re-entries when validation reports blockers_count > 0."),
|
|
46
53
|
item("compaction", "Trajectory compaction", config.compaction ? "on" : "off", CHOICES.compaction, "Summarize old turns when history approaches the model context window."),
|
|
54
|
+
item("compactionThresholdPct", "Compaction threshold (%)", String(Math.round(config.compactionThresholdPct * 100)), CHOICES.compactionThresholdPct, "Compact once estimated history tokens reach this share of the root model's context window."),
|
|
47
55
|
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."),
|
|
48
56
|
item("sandboxInitTimeoutMs", "Sandbox init timeout", String(config.sandboxInitTimeoutMs), CHOICES.sandboxInitTimeoutMs, "How long to wait for the Python worker to start."),
|
|
57
|
+
item("requestTimeoutMs", "Sandbox request timeout (min)", String(Math.round(config.requestTimeoutMs / 60_000)), CHOICES.requestTimeoutMs, "Parent-side watchdog per sandbox request; on breach the Python worker is killed."),
|
|
49
58
|
item("askUserQuestion", "[Interactive] Ask user", config.askUserQuestion ? "on" : "off", CHOICES.askUserQuestion, "Allow root REPL code to present structured ask_user_question dialogs."),
|
|
50
59
|
item("todo", "[Interactive] Todo", config.todo ? "on" : "off", CHOICES.todo, "Allow REPL code to manage a visible todo task list."),
|
|
51
60
|
item("libraryLoader", "Library loader", config.libraryLoader ? "on" : "off", CHOICES.libraryLoader,
|
|
52
|
-
"Allow load_library() to pull an external dir, file, or git repo into
|
|
61
|
+
"Allow load_library() to pull an external dir, file, or git repo into the shared context list."),
|
|
53
62
|
item("__save__", "Save & close", "↵", ["↵"], "Save these settings and close (Esc also saves)."),
|
|
54
63
|
];
|
|
55
64
|
|
|
@@ -65,7 +74,7 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
65
74
|
done();
|
|
66
75
|
return;
|
|
67
76
|
}
|
|
68
|
-
applySetting(
|
|
77
|
+
edited = applySetting(edited, id, value);
|
|
69
78
|
},
|
|
70
79
|
() => done(),
|
|
71
80
|
);
|
|
@@ -77,26 +86,37 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
77
86
|
handleInput: (data) => list.handleInput?.(data),
|
|
78
87
|
};
|
|
79
88
|
});
|
|
89
|
+
return edited;
|
|
80
90
|
}
|
|
81
91
|
|
|
82
|
-
|
|
92
|
+
/** Optional numeric field: the literal "none" clears it. */
|
|
93
|
+
function optionalNumber(value: string, scale = 1): number | undefined {
|
|
94
|
+
return value === "none" ? undefined : Number(value) * scale;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Pure: returns a new frozen config with `id` set to `value`; unknown ids pass through. */
|
|
98
|
+
export function applySetting(config: RlmConfig, id: string, value: string): RlmConfig {
|
|
83
99
|
switch (id) {
|
|
84
|
-
case "maxDepth": config
|
|
85
|
-
case "maxIterations": config
|
|
86
|
-
case "execTimeoutS": config
|
|
87
|
-
case "maxConcurrentSubcalls": config
|
|
88
|
-
case "maxBudgetUsd":
|
|
89
|
-
case "maxTimeoutMs":
|
|
90
|
-
case "maxTokens":
|
|
91
|
-
case "maxErrors":
|
|
92
|
-
case "orchestrator": config
|
|
93
|
-
case "pipeline": config
|
|
94
|
-
case "maxBackwardJumps": config
|
|
95
|
-
case "compaction": config
|
|
96
|
-
case "
|
|
97
|
-
case "
|
|
98
|
-
|
|
99
|
-
case "
|
|
100
|
-
case "
|
|
100
|
+
case "maxDepth": return Object.freeze({ ...config, maxDepth: Number(value) });
|
|
101
|
+
case "maxIterations": return Object.freeze({ ...config, maxIterations: Number(value) });
|
|
102
|
+
case "execTimeoutS": return Object.freeze({ ...config, execTimeoutS: Number(value) });
|
|
103
|
+
case "maxConcurrentSubcalls": return Object.freeze({ ...config, maxConcurrentSubcalls: Number(value) });
|
|
104
|
+
case "maxBudgetUsd": return Object.freeze({ ...config, maxBudgetUsd: optionalNumber(value) });
|
|
105
|
+
case "maxTimeoutMs": return Object.freeze({ ...config, maxTimeoutMs: optionalNumber(value, 60_000) });
|
|
106
|
+
case "maxTokens": return Object.freeze({ ...config, maxTokens: optionalNumber(value) });
|
|
107
|
+
case "maxErrors": return Object.freeze({ ...config, maxErrors: optionalNumber(value) });
|
|
108
|
+
case "orchestrator": return Object.freeze({ ...config, orchestrator: value === "on" });
|
|
109
|
+
case "pipeline": return Object.freeze({ ...config, pipeline: value === "on" });
|
|
110
|
+
case "maxBackwardJumps": return Object.freeze({ ...config, maxBackwardJumps: Number(value) });
|
|
111
|
+
case "compaction": return Object.freeze({ ...config, compaction: value === "on" });
|
|
112
|
+
case "compactionThresholdPct": return Object.freeze({ ...config, compactionThresholdPct: Number(value) / 100 });
|
|
113
|
+
case "rootSamplingMaxTokens":
|
|
114
|
+
return Object.freeze({ ...config, rootSampling: Object.freeze({ ...config.rootSampling, maxTokens: Number(value) }) });
|
|
115
|
+
case "sandboxInitTimeoutMs": return Object.freeze({ ...config, sandboxInitTimeoutMs: Number(value) });
|
|
116
|
+
case "requestTimeoutMs": return Object.freeze({ ...config, requestTimeoutMs: Number(value) * 60_000 });
|
|
117
|
+
case "askUserQuestion": return Object.freeze({ ...config, askUserQuestion: value === "on" });
|
|
118
|
+
case "todo": return Object.freeze({ ...config, todo: value === "on" });
|
|
119
|
+
case "libraryLoader": return Object.freeze({ ...config, libraryLoader: value === "on" });
|
|
120
|
+
default: return config;
|
|
101
121
|
}
|
|
102
122
|
}
|
package/src/ui/intro.ts
CHANGED
|
@@ -15,7 +15,8 @@ export const RLM_GUIDE = `# RLM mode
|
|
|
15
15
|
- \`/rlm-stop\` — cancel the current run but stay in RLM mode (use /rlm or Ctrl+Shift+R to leave)
|
|
16
16
|
- \`/rlm-help\` — show this guide again
|
|
17
17
|
|
|
18
|
-
When RLM mode is ON,
|
|
18
|
+
When RLM mode is ON, \`read\`/\`grep\` are disabled and the agent reads the repository through the
|
|
19
|
+
\`repl\` tool, delegating bulk analysis to sub-LLMs. The footer/status line shows the current state.`;
|
|
19
20
|
|
|
20
21
|
export function postRlmGuide(pi: ExtensionAPI, controller: RlmController): void {
|
|
21
22
|
const content = RLM_GUIDE.replace("{state}", formatRlmStateLine(controller));
|
package/src/ui/status.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Footer status line for RLM mode and active runs. */
|
|
2
2
|
|
|
3
|
-
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ContextUsage, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
5
5
|
import type { RlmController } from "../mode/rlm-mode.ts";
|
|
6
6
|
|
|
@@ -10,15 +10,18 @@ export function modelLabel(model: Model<Api> | undefined, fallback: string): str
|
|
|
10
10
|
return model ? `${model.provider}/${model.id}` : fallback;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function formatRlmStateLine(controller: RlmController): string {
|
|
13
|
+
export function formatRlmStateLine(controller: RlmController, contextUsage?: ContextUsage): string {
|
|
14
14
|
if (!controller.enabled) return "○ RLM OFF";
|
|
15
15
|
const worker = modelLabel(controller.workerModel, controller.savedWorkerRef ?? "cheapest");
|
|
16
16
|
const workerSuffix = controller.config.subSampling.reasoning ? `:${controller.config.subSampling.reasoning}` : "";
|
|
17
|
-
|
|
17
|
+
// `percent` is null right after a compaction, before the next assistant response reports usage.
|
|
18
|
+
const percent = contextUsage?.percent;
|
|
19
|
+
const ctxSuffix = percent === null || percent === undefined ? "" : ` · ctx ${Math.round(percent)}%`;
|
|
20
|
+
return `● RLM ON · worker=${worker}${workerSuffix}${ctxSuffix}`;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
export function setRlmModeStatus(ui: ExtensionUIContext, controller: RlmController): void {
|
|
21
|
-
ui.setStatus(KEY, formatRlmStateLine(controller));
|
|
23
|
+
export function setRlmModeStatus(ui: ExtensionUIContext, controller: RlmController, contextUsage?: ContextUsage): void {
|
|
24
|
+
ui.setStatus(KEY, formatRlmStateLine(controller, contextUsage));
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export function clearRlmStatus(ui: ExtensionUIContext): void {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme adapters bound to an *injected* Theme instance.
|
|
3
|
+
*
|
|
4
|
+
* Pi's own `getMarkdownTheme()` closes over a module-global `theme` singleton. Extensions are
|
|
5
|
+
* loaded through jiti, which gives them a separate module cache, so that global can be
|
|
6
|
+
* `undefined` inside a plugin — pi documents this footgun on `DynamicBorder`. Every renderer
|
|
7
|
+
* pi calls hands us a live `Theme`, so we build the adapter from that instead of the global.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { MarkdownTheme } from "@earendil-works/pi-tui";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A `MarkdownTheme` derived from the theme pi passed to this render pass.
|
|
15
|
+
*
|
|
16
|
+
* `highlightCode` is deliberately omitted: pi's implementation also reads the module global,
|
|
17
|
+
* and it is optional on `MarkdownTheme` — code blocks render uncoloured rather than crashing.
|
|
18
|
+
*/
|
|
19
|
+
export function markdownTheme(theme: Theme): MarkdownTheme {
|
|
20
|
+
return {
|
|
21
|
+
heading: (text) => theme.fg("mdHeading", text),
|
|
22
|
+
link: (text) => theme.fg("mdLink", text),
|
|
23
|
+
linkUrl: (text) => theme.fg("mdLinkUrl", text),
|
|
24
|
+
code: (text) => theme.fg("mdCode", text),
|
|
25
|
+
codeBlock: (text) => theme.fg("mdCodeBlock", text),
|
|
26
|
+
codeBlockBorder: (text) => theme.fg("mdCodeBlockBorder", text),
|
|
27
|
+
quote: (text) => theme.fg("mdQuote", text),
|
|
28
|
+
quoteBorder: (text) => theme.fg("mdQuoteBorder", text),
|
|
29
|
+
hr: (text) => theme.fg("mdHr", text),
|
|
30
|
+
listBullet: (text) => theme.fg("mdListBullet", text),
|
|
31
|
+
bold: (text) => theme.bold(text),
|
|
32
|
+
italic: (text) => theme.italic(text),
|
|
33
|
+
underline: (text) => theme.underline(text),
|
|
34
|
+
strikethrough: (text) => theme.strikethrough(text),
|
|
35
|
+
};
|
|
36
|
+
}
|
package/src/ui/theme.ts
CHANGED
|
@@ -1,36 +1,11 @@
|
|
|
1
1
|
/** Small presentation helpers shared by the RLM widgets (glyphs, spinner, formatting). */
|
|
2
2
|
|
|
3
|
-
import type { SubcallKind, SubcallStatus } from "../tool/rlm-details.ts";
|
|
4
|
-
|
|
5
3
|
export const SPINNER = Object.freeze(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]);
|
|
6
4
|
|
|
7
5
|
export function spinnerFrame(): string {
|
|
8
6
|
return SPINNER[Math.floor(Date.now() / 100) % SPINNER.length] ?? "⠋";
|
|
9
7
|
}
|
|
10
8
|
|
|
11
|
-
/** Glyph for a node's status. */
|
|
12
|
-
export function statusGlyph(status: SubcallStatus): string {
|
|
13
|
-
if (status === "done") return "✓";
|
|
14
|
-
if (status === "error") return "✗";
|
|
15
|
-
return spinnerFrame();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Short role label for a node kind. */
|
|
19
|
-
export function kindLabel(kind: SubcallKind): string {
|
|
20
|
-
switch (kind) {
|
|
21
|
-
case "root":
|
|
22
|
-
return "RLM ▸ root";
|
|
23
|
-
case "rlm":
|
|
24
|
-
return "rlm_query";
|
|
25
|
-
case "batch":
|
|
26
|
-
return "llm_query×";
|
|
27
|
-
case "tool":
|
|
28
|
-
return "tool";
|
|
29
|
-
default:
|
|
30
|
-
return "llm_query";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
9
|
export function formatCost(usd: number): string {
|
|
35
10
|
return `$${usd.toFixed(usd < 1 ? 4 : 2)}`;
|
|
36
11
|
}
|