@hicaru/pi-rlm 0.3.8 → 0.3.13
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 +3 -4
- package/package.json +1 -1
- package/src/bridge/handlers/completion.ts +5 -0
- package/src/bridge/handlers/emitting.ts +33 -23
- package/src/bridge/handlers/index.ts +1 -1
- package/src/bridge/handlers/llm-query.ts +23 -24
- package/src/bridge/handlers/rlm-query.ts +10 -32
- package/src/bridge/handlers/types.ts +8 -1
- package/src/bridge/model.ts +33 -15
- package/src/commands/pins.ts +51 -0
- package/src/commands/rlm-config.ts +4 -88
- package/src/commands/rlm-llm.ts +59 -0
- package/src/commands/rlm-rlm.ts +58 -0
- package/src/commands/rlm.ts +2 -2
- package/src/config/defaults.ts +14 -4
- package/src/config/settings.ts +26 -3
- package/src/core/budget.ts +1 -1
- package/src/core/compaction.ts +4 -0
- package/src/core/engine.ts +21 -4
- package/src/core/iteration.ts +12 -0
- package/src/core/ledger.ts +15 -123
- package/src/core/memory.ts +13 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/types.ts +14 -0
- package/src/index.ts +53 -4
- package/src/mode/rlm-mode.ts +11 -1
- package/src/prompts/glossary.ts +11 -3
- package/src/prompts/native.ts +1 -1
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/src/tool/repl-render.ts +4 -10
- package/src/tool/repl-tool.ts +30 -17
- package/src/tool/rlm-aggregator.ts +16 -3
- package/src/tool/rlm-details.ts +8 -0
- package/src/tool/rlm-events.ts +17 -1
- package/src/tool/rlm-tool.ts +25 -14
- package/src/tool/subcall-render.ts +14 -129
- package/src/tool/subcall-store.ts +11 -1
- package/src/ui/intro.ts +13 -4
- package/src/ui/modal/agent-modal.ts +104 -0
- package/src/ui/modal/modal-view.ts +132 -0
- package/src/ui/modal/timeline-store.ts +85 -0
- package/src/ui/model-picker/drilldown.ts +173 -0
- package/src/ui/model-picker/grouping.ts +81 -0
- package/src/ui/model-picker/levels.ts +63 -0
- package/src/ui/model-picker.ts +7 -197
- package/src/ui/panel/run-registry.ts +135 -0
- package/src/ui/panel/tree-panel.ts +46 -0
- package/src/ui/status.ts +26 -13
- package/src/ui/theme.ts +0 -4
- package/src/ui/tree/tree-model.ts +226 -0
- package/src/ui/tree/tree-rows.ts +74 -0
- package/src/ui/tree/tree-widget.ts +186 -0
- package/src/util/retry.ts +180 -0
- package/src/util/throttle.ts +90 -0
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { Markdown } from "@earendil-works/pi-tui";
|
|
5
5
|
import { registerRlmCommand } from "./commands/rlm.ts";
|
|
6
6
|
import { registerRlmConfigCommand } from "./commands/rlm-config.ts";
|
|
7
|
+
import { registerRlmLlmCommand } from "./commands/rlm-llm.ts";
|
|
8
|
+
import { registerRlmRlmCommand } from "./commands/rlm-rlm.ts";
|
|
7
9
|
import type { RlmConfig } from "./core/types.ts";
|
|
8
10
|
import { createRlmTool } from "./tool/rlm-tool.ts";
|
|
9
11
|
import { createReplTool } from "./tool/repl-tool.ts";
|
|
@@ -12,6 +14,8 @@ import { RlmController } from "./mode/rlm-mode.ts";
|
|
|
12
14
|
import { cheapestModel } from "./mode/llm-model.ts";
|
|
13
15
|
import { postRlmGuide } from "./ui/intro.ts";
|
|
14
16
|
import { setRlmModeStatus } from "./ui/status.ts";
|
|
17
|
+
import { RunRegistry } from "./ui/panel/run-registry.ts";
|
|
18
|
+
import { installTreePanel } from "./ui/panel/tree-panel.ts";
|
|
15
19
|
import { markdownTheme } from "./ui/theme-adapter.ts";
|
|
16
20
|
import { SANDBOX_WATCHDOG_HEARTBEAT_MS } from "./sandbox/sandbox.ts";
|
|
17
21
|
import { SandboxManager } from "./sandbox/sandbox-manager.ts";
|
|
@@ -19,6 +23,7 @@ import { buildSessionGates, type SubcallGates } from "./util/concurrency.ts";
|
|
|
19
23
|
import { BackgroundTasks } from "./tool/background-tasks.ts";
|
|
20
24
|
import { MemoryStore } from "./core/memory.ts";
|
|
21
25
|
import { modelComplete } from "./bridge/model.ts";
|
|
26
|
+
import { retryPolicy } from "./util/retry.ts";
|
|
22
27
|
import { resolve } from "node:path";
|
|
23
28
|
import { resolveSource } from "./context/resolve.ts";
|
|
24
29
|
import { formatContextListing } from "./context/listing.ts";
|
|
@@ -98,6 +103,19 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
98
103
|
maxTokens: config.maxTokens,
|
|
99
104
|
maxErrors: config.maxErrors,
|
|
100
105
|
});
|
|
106
|
+
// Session tree panel: every repl cell / rlm run / detached bg task registers here;
|
|
107
|
+
// the below-editor widget + agent modal read from it. Background is persistent
|
|
108
|
+
// and hides itself while idle.
|
|
109
|
+
const runRegistry = new RunRegistry();
|
|
110
|
+
runRegistry.register({
|
|
111
|
+
runId: "background",
|
|
112
|
+
label: "background tasks",
|
|
113
|
+
emitter: background.emitter,
|
|
114
|
+
subcalls: () => background.liveSubcalls(),
|
|
115
|
+
totals: () => background.liveTotals(),
|
|
116
|
+
hideWhenEmpty: true,
|
|
117
|
+
});
|
|
118
|
+
let treePanelInstalled = false;
|
|
101
119
|
// A detached child works in its OWN sandbox, so this one sees no frames and its request
|
|
102
120
|
// watchdog would fire mid-await and SIGKILL a healthy worker, taking the REPL namespace
|
|
103
121
|
// with it. Keep it alive while detached work is genuinely in flight.
|
|
@@ -161,9 +179,11 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
161
179
|
// ── Commands ──
|
|
162
180
|
registerRlmCommand(pi, controller);
|
|
163
181
|
registerRlmConfigCommand(pi, controller);
|
|
182
|
+
registerRlmLlmCommand(pi, controller);
|
|
183
|
+
registerRlmRlmCommand(pi, controller);
|
|
164
184
|
|
|
165
185
|
// ── Tool registration ──
|
|
166
|
-
pi.registerTool(createRlmTool(controller));
|
|
186
|
+
pi.registerTool(createRlmTool(controller, runRegistry));
|
|
167
187
|
let guidePosted = false;
|
|
168
188
|
|
|
169
189
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -172,6 +192,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
172
192
|
const persisted = await loadSettings();
|
|
173
193
|
controller.config = mergeConfig(persisted.config);
|
|
174
194
|
controller.savedLlmRef = persisted.llm ?? undefined;
|
|
195
|
+
controller.savedRlmRef = persisted.rlm ?? undefined;
|
|
175
196
|
|
|
176
197
|
// An explicit --rlm flag wins over the persisted setting for this session.
|
|
177
198
|
const flag = pi.getFlag("rlm");
|
|
@@ -188,6 +209,25 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
188
209
|
console.warn(`[rlm] model registry refresh failed: ${errorMessage(err)}`);
|
|
189
210
|
}
|
|
190
211
|
|
|
212
|
+
if (controller.savedRlmRef) {
|
|
213
|
+
const resolvedRlm = resolveModelId(ctx.modelRegistry, controller.savedRlmRef);
|
|
214
|
+
if (resolvedRlm) {
|
|
215
|
+
controller.rlmModel = resolvedRlm;
|
|
216
|
+
} else {
|
|
217
|
+
console.warn(
|
|
218
|
+
`[rlm] pinned rlm model ${controller.savedRlmRef} not in registry; following session model until it reappears`,
|
|
219
|
+
);
|
|
220
|
+
try {
|
|
221
|
+
ctx.ui.notify(
|
|
222
|
+
`RLM: pinned rlm=${controller.savedRlmRef} unavailable — following session model until it is`,
|
|
223
|
+
"warning",
|
|
224
|
+
);
|
|
225
|
+
} catch {
|
|
226
|
+
// Some hosts have no UI at session_start.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
191
231
|
if (controller.savedLlmRef) {
|
|
192
232
|
const resolved = resolveModelId(ctx.modelRegistry, controller.savedLlmRef);
|
|
193
233
|
if (resolved) {
|
|
@@ -217,7 +257,11 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
217
257
|
// the workspace root is only known once the session starts.
|
|
218
258
|
const consolidateModel = llmModel;
|
|
219
259
|
memory.setLlm((prompt) =>
|
|
220
|
-
modelComplete([{ role: "user", content: prompt }], {
|
|
260
|
+
modelComplete([{ role: "user", content: prompt }], {
|
|
261
|
+
model: consolidateModel,
|
|
262
|
+
registry: ctx.modelRegistry,
|
|
263
|
+
retry: retryPolicy(controller.config),
|
|
264
|
+
})
|
|
221
265
|
.then((r) => r.text));
|
|
222
266
|
memory.setRoot(ctx.cwd ?? process.cwd());
|
|
223
267
|
// v5 provider caps (audit C1/C6): ONE resolver shared by both composition roots — the
|
|
@@ -259,6 +303,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
259
303
|
gates: resolveSessionGates(),
|
|
260
304
|
resolveGates: resolveSessionGates,
|
|
261
305
|
background,
|
|
306
|
+
runRegistry,
|
|
262
307
|
memory,
|
|
263
308
|
registerDiscardHook: (reset) => { onSandboxDiscardExtra = reset; },
|
|
264
309
|
registerContextBundle: (bundle) => {
|
|
@@ -282,7 +327,11 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
282
327
|
}
|
|
283
328
|
}
|
|
284
329
|
|
|
285
|
-
setRlmModeStatus(ctx
|
|
330
|
+
setRlmModeStatus(ctx, controller, ctx.getContextUsage());
|
|
331
|
+
if (!treePanelInstalled) {
|
|
332
|
+
treePanelInstalled = true;
|
|
333
|
+
installTreePanel(ctx, runRegistry);
|
|
334
|
+
}
|
|
286
335
|
if (!guidePosted && controller.enabled) {
|
|
287
336
|
guidePosted = true;
|
|
288
337
|
postRlmGuide(pi, controller);
|
|
@@ -291,7 +340,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
291
340
|
|
|
292
341
|
// ── Keep the footer's context reading live (RLM exists to shrink this number) ──
|
|
293
342
|
pi.on("turn_end", async (_event, ctx) => {
|
|
294
|
-
setRlmModeStatus(ctx
|
|
343
|
+
setRlmModeStatus(ctx, controller, ctx.getContextUsage());
|
|
295
344
|
});
|
|
296
345
|
|
|
297
346
|
/** True when the native-mode trade holds: enabled AND repl is in the active tool set. */
|
package/src/mode/rlm-mode.ts
CHANGED
|
@@ -35,6 +35,11 @@ export class RlmController {
|
|
|
35
35
|
savedLlmRef: string | undefined;
|
|
36
36
|
/** Set by applyLlmSelection when the user explicitly picks "cheapest (auto)". */
|
|
37
37
|
explicitClearPin = false;
|
|
38
|
+
/** Pinned rlm root/worker model — when unset, child engines follow pi's session model. */
|
|
39
|
+
rlmModel: Model<Api> | undefined;
|
|
40
|
+
savedRlmRef: string | undefined;
|
|
41
|
+
/** Set by applyRlmSelection when the user explicitly picks "(follow session model)". */
|
|
42
|
+
explicitClearRlmPin = false;
|
|
38
43
|
private active: AbortController | null = null;
|
|
39
44
|
/** v5: session admission gates (provider-capped), shared with the repl() tool — set at
|
|
40
45
|
* session_start so BOTH composition roots admit through one pool (audit C1). */
|
|
@@ -78,6 +83,9 @@ export class RlmController {
|
|
|
78
83
|
llm: this.explicitClearPin
|
|
79
84
|
? null
|
|
80
85
|
: (modelRef(this.llmModel) ?? this.savedLlmRef),
|
|
86
|
+
rlm: this.explicitClearRlmPin
|
|
87
|
+
? null
|
|
88
|
+
: (modelRef(this.rlmModel) ?? this.savedRlmRef),
|
|
81
89
|
});
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -91,7 +99,9 @@ export class RlmController {
|
|
|
91
99
|
|
|
92
100
|
resolveModels(ctx: ExtensionContext): { model: Model<Api>; llm: Model<Api> } | undefined {
|
|
93
101
|
if (!this.llmModel && this.savedLlmRef) this.llmModel = resolveModelId(ctx.modelRegistry, this.savedLlmRef);
|
|
94
|
-
|
|
102
|
+
if (!this.rlmModel && this.savedRlmRef) this.rlmModel = resolveModelId(ctx.modelRegistry, this.savedRlmRef);
|
|
103
|
+
// The rlm pin wins over the session model; unset → follow pi's active model.
|
|
104
|
+
const model = this.rlmModel ?? ctx.model ?? cheapestModel(ctx.modelRegistry);
|
|
95
105
|
if (!model) return undefined;
|
|
96
106
|
const llm = this.llmModel ?? cheapestModel(ctx.modelRegistry) ?? model;
|
|
97
107
|
return { model, llm };
|
package/src/prompts/glossary.ts
CHANGED
|
@@ -357,9 +357,17 @@ export function replGlossary(
|
|
|
357
357
|
" verified result in `answers` — see the decomposition doctrine below.",
|
|
358
358
|
"- `SHOW_VARS() -> str`: list every variable currently in the REPL (Task handles show as `<Task …>`).",
|
|
359
359
|
"- `list_tasks()`: every Task this REPL created — [{kind, label, done, var}].",
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
360
|
+
...(delegation
|
|
361
|
+
? [
|
|
362
|
+
"- `memory.query(q) -> str`: durable notes under `.rlm/` that survive across sessions.",
|
|
363
|
+
" **READ-ONLY here** — `memory.add` is root-only. Query before re-studying a known area;",
|
|
364
|
+
" your own final answer is recorded as an episode automatically.",
|
|
365
|
+
]
|
|
366
|
+
: [
|
|
367
|
+
"- `memory.query(q) -> str` / `memory.add(text, paths=…, tags=…)`: durable notes under `.rlm/`",
|
|
368
|
+
" that survive across sessions. Query before re-studying a known area; add concise findings",
|
|
369
|
+
" (facts, locations, decisions) — never secrets or API keys (notes persist on disk).",
|
|
370
|
+
]),
|
|
363
371
|
"- `list_claims()`: the live `[ledger]` table of inflight/done agent work.",
|
|
364
372
|
'- `answer`: a dict initialized to {"content": "", "ready": False}. To submit your final answer,',
|
|
365
373
|
' set `answer["content"]` to the answer text and `answer["ready"] = True`.',
|
package/src/prompts/native.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Native-mode prompts — the main Pi agent drives the sandbox through the `repl` tool.
|
|
2
2
|
*
|
|
3
|
-
* Structure mirrors
|
|
3
|
+
* Structure mirrors api_v5_anthropic (best bake-off arm): role → contract → routing →
|
|
4
4
|
* few-shots → anti-patterns → REPL surface. Goal: multi-area work fires rlm_batch / rlm_query
|
|
5
5
|
* as Task (BG), not serial repl+native read.
|
|
6
6
|
*/
|
|
Binary file
|
|
Binary file
|
package/src/tool/repl-render.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
/** repl() tool TUI views —
|
|
1
|
+
/** repl() tool TUI views — the single-line card and the expanded output view.
|
|
2
|
+
* Sub-call trees are not rendered here; the live tree widget owns agent visualization. */
|
|
2
3
|
|
|
3
4
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
5
6
|
import type { ReplDetails } from "./repl-details.ts";
|
|
6
|
-
import { cardHeader, cardStatsLine, renderCollapsedCard
|
|
7
|
+
import { cardHeader, cardStatsLine, renderCollapsedCard } from "./subcall-render.ts";
|
|
7
8
|
|
|
8
9
|
/** Chars of stdout/stderr shown in the expanded view. */
|
|
9
10
|
const EXPANDED_STDOUT_CHARS = 2_000;
|
|
@@ -17,7 +18,7 @@ export function replStats(details: ReplDetails, theme: Theme): string {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
20
|
-
return renderCollapsedCard("REPL", details.status, replStats(details, theme),
|
|
21
|
+
return renderCollapsedCard("REPL", details.status, replStats(details, theme), theme);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
// ── Expanded view ──
|
|
@@ -47,12 +48,5 @@ export function renderReplExpanded(details: ReplDetails, theme: Theme): Containe
|
|
|
47
48
|
container.addChild(new Text(theme.fg("error", details.stderr.slice(0, EXPANDED_STDERR_CHARS)), 0, 0));
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
// Sub-call tree
|
|
51
|
-
if (details.subcalls.length > 0) {
|
|
52
|
-
container.addChild(new Spacer(1));
|
|
53
|
-
container.addChild(new Text(theme.fg("muted", "─── Sub-calls ───"), 0, 0));
|
|
54
|
-
container.addChild(renderExpandedSubcallTree(details.subcalls, theme));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
51
|
return container;
|
|
58
52
|
}
|
package/src/tool/repl-tool.ts
CHANGED
|
@@ -33,10 +33,12 @@ import type { MemoryStore } from "../core/memory.ts";
|
|
|
33
33
|
import { BackgroundTasks } from "./background-tasks.ts";
|
|
34
34
|
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
35
35
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
36
|
+
import type { RunRegistry } from "../ui/panel/run-registry.ts";
|
|
36
37
|
import { SubcallStore } from "./subcall-store.ts";
|
|
37
38
|
import type { ReplDetails } from "./repl-details.ts";
|
|
38
39
|
import type { RlmSubcall } from "./rlm-details.ts";
|
|
39
40
|
import { createEngine } from "../core/engine.ts";
|
|
41
|
+
import { modelRef } from "../config/settings.ts";
|
|
40
42
|
import { spinnerFrame } from "../ui/theme.ts";
|
|
41
43
|
import { previewText } from "../text/preview.ts";
|
|
42
44
|
import { errorMessage } from "../util/errors.ts";
|
|
@@ -117,6 +119,8 @@ export interface ReplToolDeps {
|
|
|
117
119
|
readonly resolveGates?: () => SubcallGates;
|
|
118
120
|
/** Session-scoped home for detached spawn() work. */
|
|
119
121
|
readonly background: BackgroundTasks;
|
|
122
|
+
/** Session tree panel index; omitted → runs don't appear in the widget. */
|
|
123
|
+
readonly runRegistry?: RunRegistry;
|
|
120
124
|
/** v5 durable memory (session-wide `.rlm` store); omitted → memory off for this tool. */
|
|
121
125
|
readonly memory?: MemoryStore;
|
|
122
126
|
readonly signal?: AbortSignal;
|
|
@@ -250,6 +254,20 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
250
254
|
|
|
251
255
|
const emitter = new RlmEmitter();
|
|
252
256
|
const store = new SubcallStore(emitter);
|
|
257
|
+
// Surface the cell in the tree panel for its whole lifetime (unregistered in finally).
|
|
258
|
+
const unregisterRun = deps.runRegistry?.register({
|
|
259
|
+
runId: `repl-${_toolCallId}`,
|
|
260
|
+
label: `repl: ${previewText(params.code, 40)}`,
|
|
261
|
+
emitter,
|
|
262
|
+
subcalls: () => store.getSubcalls(),
|
|
263
|
+
totals: () => store.getTotals(),
|
|
264
|
+
// Root = the session's default (pi) model driving this repl cell, own spend only.
|
|
265
|
+
rootModel: () => {
|
|
266
|
+
const m = getModel();
|
|
267
|
+
return modelRef(m) ?? m.id;
|
|
268
|
+
},
|
|
269
|
+
rootTokens: () => store.getRootUsage().tokens,
|
|
270
|
+
});
|
|
253
271
|
let capturedStdout = "";
|
|
254
272
|
let capturedStderr = "";
|
|
255
273
|
let progressStatus: ReplDetails["status"] = "running";
|
|
@@ -319,23 +337,17 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
319
337
|
}
|
|
320
338
|
|
|
321
339
|
const start = Date.now();
|
|
322
|
-
// v5 blackboard (audit C3 /
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
// emitter/limits for the currently running REPL execution.
|
|
334
|
-
bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits });
|
|
335
|
-
}, execSignal);
|
|
336
|
-
} finally {
|
|
337
|
-
if (ledgerActive) sessionLedger.endNativeCell(ancestorN);
|
|
338
|
-
}
|
|
340
|
+
// v5 blackboard (audit C3 / BUG-1): ancestors are RUNNING engines only — engine.ts
|
|
341
|
+
// brackets each child run with beginRun/endRun. A native cell pushes NOTHING: its
|
|
342
|
+
// spawns claim against an empty stack, so an originator can never echo against
|
|
343
|
+
// itself. Duplicates are caught by the ledger's claim store (exact/near
|
|
344
|
+
// coalescing + rlmBudget demotion), never by silent suppression.
|
|
345
|
+
const result: ReplResult = await sandboxManager.execWithSetup(params.code, () => {
|
|
346
|
+
// Wire per-invocation mutable state only after the serialized exec slot
|
|
347
|
+
// is active. Swapping earlier would let queued repl() calls overwrite
|
|
348
|
+
// emitter/limits for the currently running REPL execution.
|
|
349
|
+
bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits });
|
|
350
|
+
}, execSignal);
|
|
339
351
|
const elapsed = Date.now() - start;
|
|
340
352
|
capturedStdout = result.stdout;
|
|
341
353
|
capturedStderr = result.stderr;
|
|
@@ -427,6 +439,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
427
439
|
};
|
|
428
440
|
} finally {
|
|
429
441
|
progress.stop();
|
|
442
|
+
unregisterRun?.();
|
|
430
443
|
for (const off of detachTracers) off();
|
|
431
444
|
store.dispose();
|
|
432
445
|
emitter.shutdown();
|
|
@@ -13,16 +13,17 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
|
|
16
|
-
import type { RlmEmitter, TurnEvent, RootUsageEvent, AnswerEvent, StatusEvent, RootPromptEvent } from "./rlm-events.ts";
|
|
17
|
-
import type { RlmDetails, RlmRunStatus } from "./rlm-details.ts";
|
|
16
|
+
import type { RlmEmitter, TurnEvent, RootUsageEvent, AnswerEvent, StatusEvent, RootPromptEvent, RootPhaseEvent } from "./rlm-events.ts";
|
|
17
|
+
import type { RlmDetails, RlmRunStatus, SubcallPhase } from "./rlm-details.ts";
|
|
18
18
|
import { EmitterListener } from "./emitter-listener.ts";
|
|
19
|
-
import { SubcallStore } from "./subcall-store.ts";
|
|
19
|
+
import { SubcallStore, type SubcallTotals } from "./subcall-store.ts";
|
|
20
20
|
|
|
21
21
|
export class RlmEventAggregator extends EmitterListener {
|
|
22
22
|
private readonly store: SubcallStore;
|
|
23
23
|
|
|
24
24
|
// Root-level state
|
|
25
25
|
private rootStatus: RlmRunStatus = "running";
|
|
26
|
+
private rootPhase?: SubcallPhase;
|
|
26
27
|
private rootPrompt = "";
|
|
27
28
|
private turnCurrent = 0;
|
|
28
29
|
private turnMax = 0;
|
|
@@ -41,6 +42,7 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
41
42
|
emitter.onAnswer((e) => this.handleAnswer(e)),
|
|
42
43
|
emitter.onStatus((e) => this.handleStatus(e)),
|
|
43
44
|
emitter.onRootPrompt((e) => this.handleRootPrompt(e)),
|
|
45
|
+
emitter.onRootPhase((e) => this.handleRootPhase(e)),
|
|
44
46
|
]);
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -72,12 +74,18 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
72
74
|
// No notify — root prompt is set before listeners exist; no TUI re-render needed
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
private handleRootPhase(event: RootPhaseEvent): void {
|
|
78
|
+
this.rootPhase = event.phase;
|
|
79
|
+
this.notify();
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
// ── Read ──
|
|
76
83
|
|
|
77
84
|
/** Snapshot the current accumulated state. O(1). */
|
|
78
85
|
getState(): RlmDetails {
|
|
79
86
|
return {
|
|
80
87
|
status: this.rootStatus,
|
|
88
|
+
rootPhase: this.rootPhase,
|
|
81
89
|
rootPrompt: this.rootPrompt,
|
|
82
90
|
turns: { current: this.turnCurrent, max: this.turnMax },
|
|
83
91
|
subcalls: this.store.getSubcalls(),
|
|
@@ -86,6 +94,11 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
86
94
|
};
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
/** Root engine's OWN spend (driver-model turns) — never blends sub-call models. */
|
|
98
|
+
getRootUsage(): SubcallTotals {
|
|
99
|
+
return this.store.getRootUsage();
|
|
100
|
+
}
|
|
101
|
+
|
|
89
102
|
// ── Lifecycle ──
|
|
90
103
|
|
|
91
104
|
/** Detach all emitter listeners. Call after the run completes. */
|
package/src/tool/rlm-details.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
export type SubcallKind = "root" | "rlm" | "llm" | "batch" | "tool";
|
|
10
10
|
export type SubcallStatus = "running" | "done" | "error";
|
|
11
|
+
|
|
12
|
+
/** Live activity of a node while status is "running" — powers the tree/modal UI.
|
|
13
|
+
* "queued" = parked on the rate-limit cooldown (util/throttle.ts), not in flight. */
|
|
14
|
+
export type SubcallPhase = "thinking" | "texting" | "repl" | "waiting" | "spawning" | "queued";
|
|
11
15
|
export type RlmRunStatus = "running" | "done" | "error" | "aborted";
|
|
12
16
|
|
|
13
17
|
export interface RlmSubcall {
|
|
@@ -20,6 +24,8 @@ export interface RlmSubcall {
|
|
|
20
24
|
readonly label: string;
|
|
21
25
|
readonly model?: string;
|
|
22
26
|
readonly status: SubcallStatus;
|
|
27
|
+
/** Current activity while running (undefined = not reported). */
|
|
28
|
+
readonly phase?: SubcallPhase;
|
|
23
29
|
readonly detail?: string;
|
|
24
30
|
readonly args?: string;
|
|
25
31
|
readonly resultPreview?: string;
|
|
@@ -35,6 +41,8 @@ export interface RlmSubcall {
|
|
|
35
41
|
|
|
36
42
|
export interface RlmDetails {
|
|
37
43
|
readonly status: RlmRunStatus;
|
|
44
|
+
/** Root node's live activity phase (root has no subcall entry). */
|
|
45
|
+
readonly rootPhase?: SubcallPhase;
|
|
38
46
|
readonly rootPrompt: string;
|
|
39
47
|
readonly turns: { readonly current: number; readonly max: number };
|
|
40
48
|
readonly subcalls: readonly RlmSubcall[];
|
package/src/tool/rlm-events.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { EventEmitter } from "node:events";
|
|
14
|
-
import type { SubcallKind, SubcallStatus, RlmRunStatus } from "./rlm-details.ts";
|
|
14
|
+
import type { SubcallKind, SubcallStatus, RlmRunStatus, SubcallPhase } from "./rlm-details.ts";
|
|
15
15
|
|
|
16
16
|
// ── Event payloads ──
|
|
17
17
|
|
|
@@ -32,6 +32,8 @@ export interface SubcallCreatedEvent {
|
|
|
32
32
|
export interface SubcallUpdatedEvent {
|
|
33
33
|
readonly id: string;
|
|
34
34
|
readonly status?: SubcallStatus;
|
|
35
|
+
/** Live activity while running (thinking/repl/waiting…). */
|
|
36
|
+
readonly phase?: SubcallPhase;
|
|
35
37
|
readonly detail?: string;
|
|
36
38
|
readonly args?: string;
|
|
37
39
|
readonly resultPreview?: string;
|
|
@@ -67,6 +69,10 @@ export interface RootPromptEvent {
|
|
|
67
69
|
readonly text: string;
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
export interface RootPhaseEvent {
|
|
73
|
+
readonly phase: SubcallPhase;
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
// ── RlmEmitter ──
|
|
71
77
|
|
|
72
78
|
/**
|
|
@@ -127,6 +133,11 @@ export class RlmEmitter {
|
|
|
127
133
|
this.ee.emit("root-prompt", { text } satisfies RootPromptEvent);
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/** Set the root node's live activity phase (root-only; children use subcall updates). */
|
|
137
|
+
emitRootPhase(phase: SubcallPhase): void {
|
|
138
|
+
this.ee.emit("root-phase", { phase } satisfies RootPhaseEvent);
|
|
139
|
+
}
|
|
140
|
+
|
|
130
141
|
// ── Subscribe (returns unsubscribe function) ──
|
|
131
142
|
|
|
132
143
|
onSubcallCreated(handler: (event: SubcallCreatedEvent) => void): () => void {
|
|
@@ -149,6 +160,11 @@ export class RlmEmitter {
|
|
|
149
160
|
return () => { this.ee.off("root-usage", handler); };
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
onRootPhase(handler: (event: RootPhaseEvent) => void): () => void {
|
|
164
|
+
this.ee.on("root-phase", handler);
|
|
165
|
+
return () => { this.ee.off("root-phase", handler); };
|
|
166
|
+
}
|
|
167
|
+
|
|
152
168
|
onAnswer(handler: (event: AnswerEvent) => void): () => void {
|
|
153
169
|
this.ee.on("answer", handler);
|
|
154
170
|
return () => { this.ee.off("answer", handler); };
|
package/src/tool/rlm-tool.ts
CHANGED
|
@@ -9,19 +9,16 @@ 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 type { RlmController, StartInput } from "../mode/rlm-mode.ts";
|
|
12
|
+
import { modelRef } from "../config/settings.ts";
|
|
12
13
|
import { spinnerFrame } from "../ui/theme.ts";
|
|
14
|
+
import type { RunRegistry } from "../ui/panel/run-registry.ts";
|
|
13
15
|
import { markdownTheme } from "../ui/theme-adapter.ts";
|
|
14
16
|
import { previewText } from "../text/preview.ts";
|
|
15
17
|
import { errorMessage } from "../util/errors.ts";
|
|
16
18
|
import { type RlmDetails } from "./rlm-details.ts";
|
|
17
19
|
import { RlmEmitter } from "./rlm-events.ts";
|
|
18
20
|
import { RlmEventAggregator } from "./rlm-aggregator.ts";
|
|
19
|
-
import {
|
|
20
|
-
cardHeader,
|
|
21
|
-
cardStatsLine,
|
|
22
|
-
renderCollapsedCard,
|
|
23
|
-
renderExpandedSubcallTree,
|
|
24
|
-
} from "./subcall-render.ts";
|
|
21
|
+
import { cardHeader, cardStatsLine, renderCollapsedCard } from "./subcall-render.ts";
|
|
25
22
|
import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
|
|
26
23
|
|
|
27
24
|
/** Chars of the prompt shown on the tool call line. */
|
|
@@ -43,7 +40,7 @@ function rootStats(details: RlmDetails, theme: Theme): string {
|
|
|
43
40
|
|
|
44
41
|
// ── Tool definition ──
|
|
45
42
|
|
|
46
|
-
export function createRlmTool(controller: RlmController): ToolDefinition<typeof RlmToolParams, RlmDetails> {
|
|
43
|
+
export function createRlmTool(controller: RlmController, runRegistry?: RunRegistry): ToolDefinition<typeof RlmToolParams, RlmDetails> {
|
|
47
44
|
return {
|
|
48
45
|
name: "rlm",
|
|
49
46
|
label: "RLM",
|
|
@@ -65,6 +62,25 @@ export function createRlmTool(controller: RlmController): ToolDefinition<typeof
|
|
|
65
62
|
const aggregator = new RlmEventAggregator(emitter, onUpdate ?? (() => {}));
|
|
66
63
|
emitter.emitRootPrompt(params.prompt);
|
|
67
64
|
|
|
65
|
+
// Surface the run in the tree panel for its whole lifetime (unregistered in finally).
|
|
66
|
+
const unregisterRun = runRegistry?.register({
|
|
67
|
+
runId: `rlm-${_toolCallId}`,
|
|
68
|
+
label: previewText(params.prompt, 48),
|
|
69
|
+
emitter,
|
|
70
|
+
subcalls: () => aggregator.getState().subcalls,
|
|
71
|
+
totals: () => aggregator.getState().totals,
|
|
72
|
+
rootStatus: () => aggregator.getState().status,
|
|
73
|
+
rootPhase: () => aggregator.getState().rootPhase,
|
|
74
|
+
turns: () => aggregator.getState().turns,
|
|
75
|
+
// Reuses controller.resolveModels — the ONE model-resolution path (DRY); lazy so
|
|
76
|
+
// the pin resolution inside start() is reflected, and it shows own spend only.
|
|
77
|
+
rootModel: () => {
|
|
78
|
+
const m = controller.resolveModels(ctx)?.model;
|
|
79
|
+
return m === undefined ? undefined : modelRef(m) ?? m.id;
|
|
80
|
+
},
|
|
81
|
+
rootTokens: () => aggregator.getRootUsage().tokens,
|
|
82
|
+
});
|
|
83
|
+
|
|
68
84
|
// Wire abort signal to controller
|
|
69
85
|
if (signal) {
|
|
70
86
|
signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
@@ -102,6 +118,7 @@ export function createRlmTool(controller: RlmController): ToolDefinition<typeof
|
|
|
102
118
|
};
|
|
103
119
|
} finally {
|
|
104
120
|
progress.stop();
|
|
121
|
+
unregisterRun?.();
|
|
105
122
|
aggregator.dispose();
|
|
106
123
|
emitter.shutdown();
|
|
107
124
|
}
|
|
@@ -134,12 +151,6 @@ function renderExpanded(details: RlmDetails, theme: Theme): Component {
|
|
|
134
151
|
const container = new Container();
|
|
135
152
|
container.addChild(new Text(cardHeader("RLM", details.status, rootStats(details, theme), theme), 0, 0));
|
|
136
153
|
|
|
137
|
-
if (details.subcalls.length > 0) {
|
|
138
|
-
container.addChild(new Spacer(1));
|
|
139
|
-
container.addChild(new Text(theme.fg("muted", "─── Sub-calls ───"), 0, 0));
|
|
140
|
-
container.addChild(renderExpandedSubcallTree(details.subcalls, theme));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
154
|
if (details.answer) {
|
|
144
155
|
container.addChild(new Spacer(1));
|
|
145
156
|
container.addChild(new Text(theme.fg("muted", "─── Answer ───"), 0, 0));
|
|
@@ -152,5 +163,5 @@ function renderExpanded(details: RlmDetails, theme: Theme): Component {
|
|
|
152
163
|
// ── Collapsed view ──
|
|
153
164
|
|
|
154
165
|
function renderCollapsed(details: RlmDetails, theme: Theme): Text {
|
|
155
|
-
return renderCollapsedCard("RLM", details.status, rootStats(details, theme),
|
|
166
|
+
return renderCollapsedCard("RLM", details.status, rootStats(details, theme), theme);
|
|
156
167
|
}
|